0

Loose comparison (==) vs strict comparison (===) — every edge case

Beginner5 min read·php-02-005
interview

Concept

The difference between == (loose equality) and === (strict equality) is one of the most common PHP interview questions, but most resources only scratch the surface. Let's go deep on every edge case.

The fundamental difference

=== (strict): Returns true only if both value AND type are identical. No conversion. == (loose): Applies type juggling first, then compares. The type rules depend on the types involved.

The loose comparison rules in PHP 8 (simplified):

  1. null == null → true
  2. null == anything-else → false (PHP 8 changed: null no longer equals 0 or "")
  3. bool on either side → convert both to bool, compare
  4. Both strings, both numeric → compare as numbers
  5. One string, one int/float → convert string to number if it's numeric; otherwise, in PHP 8, convert int to string
  6. Both strings, non-numeric → compare as strings (byte-by-byte)

The full loose comparison matrix (PHP 8)

LeftRight== result
nullfalsetrue
null0false (changed PHP 8!)
null""false (changed PHP 8!)
null"0"false
false0true
false""true
false"0"true
false[]true
true1true
true"1"true
true"abc"true (any non-empty non-"0" string == true)
0"0"true
0"abc"false (changed PHP 8!)
"1""01"true (both numeric strings)
"10""1e1"true (1e1 == 10.0)

Why === is almost always correct

With ===, there are no surprises. 1 === "1" is false. null === false is false. 0 === "" is false. The only cognitive overhead is remembering that objects are === only if they are the same instance (not just equal values).

Object comparison edge case

$a == $b for objects: true if same class with equal property values. $a === $b for objects: true only if the same instance (same memory address).

php
$a = new stdClass(); $a->x = 1;
$b = new stdClass(); $b->x = 1;
var_dump($a == $b);   // true — same class, equal properties
var_dump($a === $b);  // false — different instances
$c = $a;
var_dump($a === $c);  // true — same instance

Code Example

php
<?php
declare(strict_types=1);

// Comprehensive comparison demonstrations

// The canonical gotcha list
$comparisons = [
    // Type, Value A, Value B, Expected ==
    [null, false, '??'],
    [null, 0, '??'],
    [null, '', '??'],
    [false, 0, '??'],
    [false, '', '??'],
    [false, '0', '??'],
    [false, [], '??'],
    ['1', '01', '??'],
    ['10', '1e1', '??'],
    [0, '0', '??'],
    [0, 'abc', '??'],   // PHP 8: false; PHP 7: true
];

// Always use === for password/token comparison
function verifyToken(string $providedToken, string $storedToken): bool
{
    // hash_equals for timing-attack safety, === for type safety
    return hash_equals($storedToken, $providedToken);
}

// Common bug: switch uses loose comparison!
$value = "0";
switch ($value) {
    case false:
        echo "Matched false! (loose == comparison in switch)\n"; // This runs!
        break;
    case "0":
        echo "Matched string '0'\n";
        break;
}

// Fix: use match — it uses === always
$result = match($value) {
    false   => "Matched false",
    "0"     => "Matched string '0'",  // This runs correctly
    default => "No match",
};
echo $result . "\n";

// Object comparison
class Point {
    public function __construct(
        public readonly int $x,
        public readonly int $y,
    ) {}
}

$p1 = new Point(1, 2);
$p2 = new Point(1, 2);
$p3 = $p1;

var_dump($p1 == $p2);   // true — equal properties
var_dump($p1 === $p2);  // false — different instances
var_dump($p1 === $p3);  // true — same instance

// NaN comparison — a pure IEEE 754 gotcha
$nan = NAN;
var_dump($nan == $nan);   // false — NaN is never equal to anything, including itself
var_dump($nan === $nan);  // false — same!
var_dump(is_nan($nan));   // true — use this to check for NaN

// Array comparison
$arr1 = ['a' => 1, 'b' => 2];
$arr2 = ['b' => 2, 'a' => 1];
var_dump($arr1 == $arr2);   // true — same key/value pairs
var_dump($arr1 === $arr2);  // false — different key ORDER for ===

// in_array gotcha — always use strict third argument
$ids = [0, 1, 2, 3];
var_dump(in_array("abc", $ids));         // PHP 7: true! PHP 8: false
var_dump(in_array("abc", $ids, true));   // Always false — use this!

Interview Q&A

Q: PHP's switch statement uses loose comparison — what are the implications and how do you avoid the bugs?

switch uses == (loose comparison) for all case matching, which means switch("0") will match case false: before it reaches case "0":. This is a silent bug: you think you're matching a string but loose comparison matches a falsy value first. The fix is to use PHP 8's match expression, which uses strict === comparison always and throws an UnhandledMatchError if no arm matches (instead of silently falling through). For legacy code you must use switch, add a comment warning about loose comparison and ensure your case values are in the correct type order (more specific types first, no mixing of types like int 0 with strings).


Q: Why does NAN !== NAN and how do you check for NaN in PHP?

NAN (Not A Number) is an IEEE 754 floating-point concept representing undefined or unrepresentable results (like 0/0 or sqrt(-1)). By the IEEE 754 specification, NaN is never equal to anything, including itself — NAN == NAN and NAN === NAN both return false. This is mandated by the standard, not a PHP quirk (JavaScript and Python have the same behavior). To check if a value is NaN, use is_nan($value). To check for infinite values, use is_infinite(). For safe numeric comparisons with potential NaN, use is_finite($value) which returns true only for regular, non-NaN, non-infinite floats.


Q: What changed in PHP 8 regarding loose comparison with null?

Before PHP 8, null == 0 was true and null == "" was true because null coerced to 0 (int) and "" (string) respectively, both of which equal their counterparts under loose comparison. PHP 8 changed this: null only equals null and false under loose comparison. Everything else returns false. This fixed a class of subtle bugs where checking a nullable return value against 0 would pass incorrectly. The practical advice: use === null to check for null (not == null, == false, or !isset()), and only use == when you genuinely need type coercion, which is almost never.