Loose comparison (==) vs strict comparison (===) — every edge case
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):
- null == null → true
- null == anything-else → false (PHP 8 changed: null no longer equals
0or"") - bool on either side → convert both to bool, compare
- Both strings, both numeric → compare as numbers
- One string, one int/float → convert string to number if it's numeric; otherwise, in PHP 8, convert int to string
- Both strings, non-numeric → compare as strings (byte-by-byte)
The full loose comparison matrix (PHP 8)
| Left | Right | == result |
|---|---|---|
null | false | true |
null | 0 | false (changed PHP 8!) |
null | "" | false (changed PHP 8!) |
null | "0" | false |
false | 0 | true |
false | "" | true |
false | "0" | true |
false | [] | true |
true | 1 | true |
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).
$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 instanceCode Example
<?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.