In PHP 8, what is the difference between the == and === operators, and why do most codebases default to ===?
answer
- value only vs value plus type
- type juggling happens before ==
- number vs non-numeric string changed in 8.0
- bool or null operand: both become bool
- != and <> loose, !== strict
basics
~20 s== compares values after type juggling, so an int and a string can be equal; === is true only when type and value both match. Loose rules still surprise after PHP 8.0, so === is the safe default.
solid answer
~40 s`==` is **loose equality**: when the operand types differ, PHP converts one or both before comparing, so `1 == "1"` and `null == false` are true. `===` is **identity**: it is true only when both operands have the same type and the same value, with no conversion. PHP 8.0 removed the worst loose case: a number compared with a *non-numeric* string is now compared as strings, so `0 == "foo"` became false. The other rules stayed: two numeric strings still compare as numbers (`"1" == "01"` is true) and a `bool` or `null` operand still casts both sides to `bool`. Because a reader has to replay those rules to predict `==`, most teams use `===` and `!==` by default and reach for `==` only when conversion is the intent.
code
php · 8 lines<?php
var_dump(0 == 'foo'); // bool(false) since PHP 8.0, true before
var_dump('1' == '01'); // bool(true): numeric strings compare as numbers
var_dump(100 == '1e2'); // bool(true)
var_dump(null == false); // bool(true): both sides cast to bool
var_dump('abc' == true); // bool(true)
var_dump('1' === '01'); // bool(false): same type, different value
var_dump(1 === 1.0); // bool(false): int vs floatgo deeper
Know that == juggles types and === checks type and value, and give one example that differs, such as 1 == "1" versus 1 === "1".
Explain the comparison table: numeric strings compare as numbers, a bool or null operand casts both sides to bool, and PHP 8.0 changed number versus non-numeric string.
Show that PHP 8.0 fixed only one case, name the ones that remain (numeric strings, bool casts), and argue for === plus explicit casts in reviewed code.
Frame loose comparison as a codebase policy question: enforce strict comparison through review rules and static analysis rather than individual memory of the juggling table.
## Two operators, two questions PHP has two equality operators, and they ask different questions: - **`==` (loose equality)** asks: *after converting the operands to a common type, are the values equal?* The conversion is called **type juggling**. - **`===` (identity, strict equality)** asks: *do both operands have the same type and the same value?* No conversion happens. An `int` is never identical to a `string`, and `1 === 1.0` is false because one is `int` and the other `float`. Their negations follow the same split: `!=` and `<>` are the same loose inequality, `!==` is the strict one. ## What == does before it compares The manual's "Comparison with Various Types" rules decide how `==` converts. The ones that matter in everyday code: | Operands | What PHP does | Example | |---|---|---| | two numeric strings | compares them as numbers | `"1" == "01"` is true | | number and numeric string | compares as numbers | `100 == "1e2"` is true | | number and non-numeric string | since 8.0: casts the number to string, compares strings | `0 == "foo"` is false | | `bool` or `null` and anything | converts both sides to `bool` | `null == false` is true | | `null` and a string | treats `null` as `""` | `null == ""` is true | | two arrays | same key/value pairs, any order, loose values | `[1 => 'b', 0 => 'a'] == [0 => 'a', 1 => 'b']` is true | For arrays, `===` additionally requires the same order and the same types for every element. ## The PHP 8.0 change Before PHP 8.0, comparing a number with *any* string converted the string to a number first, and a string with no leading digits became `0`. That made `0 == "foo"` true, which produced real bugs in lookups and validation. PHP 8.0 changed the rule for **non-numeric** strings only: | Comparison | PHP 7.x | PHP 8.0 and later | |---|---|---| | `0 == "0"` | true | true | | `0 == "foo"` | true | **false** | | `0 == ""` | true | **false** | | `42 == "42foo"` | true | **false** | | `42 == " 42"` | true | true | What counts as a numeric string (leading and trailing whitespace, exponent notation) is its own topic; the point for equality is that numeric strings still compare numerically. ## Cases that still surprise in PHP 8.5 PHP 8.0 did not make `==` safe. These all hold on the current release: 1. `"1e3" == "1000"` is **true**: both are numeric strings, so they compare as numbers. 2. `"abc" == true` is **true**: a `bool` operand casts the string to `bool`, and a non-empty string other than `"0"` is `true`. 3. `null == 0` is **true**, and so is `null == []`: both sides become `false`. 4. `"0" == false` is **true**, while `"0.0" == false` is false, because only `""` and `"0"` are falsy strings. None of these is a bug; each follows the table. The problem is that a reader has to run the table in their head, and the value that arrives at runtime (from a form, a JSON body, a database driver) may not have the type the author imagined. ## Why === is the default Most style guides and reviewers default to `===` for three reasons: - **Predictability.** `===` has one rule. `==` has a table that changed in 8.0 and still depends on the runtime type of both operands. - **Security.** Loose comparison has a history of authentication and token-check bypasses, where an attacker supplies a value whose juggled form matches. - **Static analysis.** Tools that flag `==` in reviews exist precisely because the intent of `==` is ambiguous. `declare(strict_types=1)` does **not** change this. It controls whether scalar arguments and return values are coerced at function-call boundaries; the `==` operator keeps its juggling rules in a strict file. ## When == is still reasonable Loose comparison is not forbidden. It is a deliberate choice when you *want* conversion, for example comparing two values you already know are numeric but that may arrive as `int` or numeric `string`. Even then, many teams prefer an explicit cast followed by `===`, because the cast documents the intent in the code rather than in the reader's memory of the table.
- Does declare(strict_types=1) make == behave like === in that file?No. `strict_types` only changes how scalar arguments and return values are checked when calls are made from that file: a mismatched scalar throws `TypeError` instead of being coerced. The `==` operator still type-juggles exactly as before, so you still need `===` for strict comparison.
- How do == and === differ when both operands are arrays?For arrays, `==` is true when both have the same key/value pairs, in any order, with values compared loosely. `===` also requires the same order and the same types for every value. So `[1 => 'b', 0 => 'a'] == [0 => 'a', 1 => 'b']` is true, but `===` on them is false.
- Why is null == 0 true in PHP 8 when 0 == "foo" is not?They hit different rules. When either operand is `bool` or `null`, PHP converts both to `bool`, and `null` and `0` are both `false`. The PHP 8.0 change only affected a number compared with a non-numeric string, which is now compared as strings.
saying these in an interview costs you the question
- Claims === converts both operands to a common type before comparing.
- Says PHP 8 made == strict, so loose-comparison bugs are gone.
- Believes declare(strict_types=1) turns every == into ===.
- Thinks 0 == "abc" is still true on PHP 8.
- Thinks null == false is false because the types differ.