In PowerShell, why is $null -lt 0 = $true? Is that reliable? In PowerShell, why is $null -lt 0 = $true? Is that reliable? powershell powershell

In PowerShell, why is $null -lt 0 = $true? Is that reliable?


Why is that?

The behavior is counterintuitive:

Operators -lt, -le, -gt, -ge, even though they can also have numeric meaning, seemingly treat a $null operand as if it were the empty string (''), i.e. they default to string comparison, as the sample commands in postanote's helpful answer imply.

That is, $null -lt 0 is in effect evaluated the same as '' -lt '0', which explains the $true result, because in lexical comparison the condition is met.
While you can conceive of $null -eq 0 as '' -eq '0' too, the -eq case is special - see below.

Additionally, placing the 0 on the LHS still acts like a string comparison (except with -eq see below) - even though it is normally the type of the LHS that causes the RHS to be coerced to the same type.

That is, 0 -le $null too seems to act like '0' -le '' and therefore returns $false.

While such behavior is to be expected in operators that are exclusively string-based, such as -match and -like, it is surprising for operators that also support numbers, especially given that other such operators - as well as those that are exclusively numeric - default to numeric interpretation of $null, as 0.

  • +, -, and / do force a LHS $null to 0 ([int] by default); e.g. $null + 0 is 0
  • * does not; e.g., $null * 0 is again $null.

Of these, - and / are exclusively numeric, whereas + and * also work in string and array contexts.

There is an additional inconsistency: -eq never performs type coercion on a $null operand:

  • $null -eq <RHS> is only ever $true if <RHS> is also $null (or "automation null" - see below), and is currently the only way to reliably test a value for being $null. (To put it differently: $null -eq '' is not the same as '' -eq '' - no type coercion takes place here.)

    • GitHub PR #10704, which has unfortunately stalled, aims to implement a dedicated syntax for $null tests, such as <LHS> -is $null.
  • Similarly, <LHS> -eq $null also performs no type coercion on $null and returns $true only with $null as the LHS;

    • However, with an array-valued <LHS>, -eq acts as filter (as most operators do), returning the subarray of elements that are $null; e.g., 1, $null, 2, $null, 3 -eq $null returs 2-element array $null, $null.
    • This filtering behavior is the reason that only $null -eq <RHS> - with $null as the scalar LHS - is reliable as a test for (scalar) $null.

Note that the behaviors equally apply to the "automation null" value that PowerShell uses to express the (non-)output from commands (technically, the [System.Management.Automation.Internal.AutomationNull]::Value singleton), because this value is treated the same as $null in expressions; e.g. $(& {}) -lt 0 is also $true - see this answer for more information.

Similarly, the behaviors also apply to instances of nullable value types that happen to contain $null (e.g., [System.Nullable[int]] $x = $null; $x -lt 0 is also $true)Thanks, Dávid Laczkó., though note that their use in PowerShell is rare.


Can and should this result be relied on?

Since the behavior is inconsistent across operators, I wouldn't rely on it, not least because it's also hard to remember which rules apply when - and there's at least a hypothetical chance that the inconsistency will be fixed; given that this would amount to a breaking change, however, that may not happen.

If backward compatibility weren't a concern, the following behavior would remove the inconsistencies and make for rules that are easy to conceptualize and remember:

When a (fundamentally scalar) binary operator is given a $null operand as well as a non-$null operand - irrespective of which is the LHS and which is the RHS:

  • For operators that operate exclusively on numeric / Boolean / string operands (e.g. / / -and / -match): coerce the $null operand to the type implied by the operator.

  • For operators that operate in multiple "domains" - both textual and numeric (e.g. -eq) - coerce the $null operand to the other operand's type.

Note that this would then additionally require a dedicated $null test with different syntax, such as the -is $null from the above-mentioned PR.

Note: The above does not apply to the collection operators, -in and -contains (and their negated variants -notin and -notcontains), because their element-wise equality comparison acts like -eq and therefore never applies type coercion to $null values.


what is the best (i.e. most concise, best performing, etc.) way to reliably test for integer values (or other types for that matter) in a variable that might have a value of $null?

The following solutions force a $null operand to 0:

  • Note: (...) around the LHS of the -lt operations below is used for conceptual clarity, but isn't strictly necessary - see about_Operator_Precedence.

In PowerShell (Core) 7+, use ??, the null-coalescing operator, which works with operands of any type:

# PowerShell 7+ only($null ?? 0) -lt 0 # -> $false

In Windows PowerShell, where this operator isn't supported, use a dummy calculation:

# Windows PowerShell(0 + $null) -lt 0  # -> $false

While something like [int] $null -lt 0 works too, it requires you to commit to a specific numeric type, so if the operand happens to be higher than [int]::MaxValue, the expression will fail; [double] $null -lt 0 would minimize that risk, though could at least hypothetically result in loss of accuracy.

The dummy addition (0 +) bypasses this problem and lets PowerShell apply its usual on-demand type-widening.

As an aside: This automatic type-widening can exhibit unexpected behavior too, because an all-integer calculation whose result requires a wider type than either operand's type can fit is always widened to [double], even when a larger integer type would suffice; e.g. ([int]::MaxValue + 1).GetType().Name returns Double, even though a [long] result would have sufficed, resulting in potential loss of accuracy - see this answer for more information.


Test $null comparison results

(0).GetType()('').GetType()(' ').GetType()($null).GetType()# Results<#IsPublic IsSerial Name                                     BaseType-------- -------- ----                                     --------True     True     Int32                                    System.ValueTypeTrue     True     String                                   System.ObjectTrue     True     String                                   System.ObjectYou cannot call a method on a null-valued expression.#>Measure-Object -InputObject (0).GetType()Measure-Object -InputObject ('').GetType()Measure-Object -InputObject (' ').GetType()Measure-Object -InputObject ($null).GetType()# Results<#Count    : 1Average  : Sum      : Maximum  : Minimum  : Property : Count    : 1Average  : Sum      : Maximum  : Minimum  : Property : Count    : 1Average  : Sum      : Maximum  : Minimum  : Property : You cannot call a method on a null-valued expression.#>$Null -eq '' [string]$Null -eq ''$Null -eq [string]''[string]$Null -eq [string]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [bool]$Null -eq ''$Null -eq [bool]''[bool]$Null -eq [bool]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [int]$Null -eq ''$Null -eq [int]''[int]$Null -eq [int]''# Results<#FalseTrueFalseTrue#>$Null -eq '' [double]$Null -eq ''$Null -eq [double]''[double]$Null -eq [double]''# Results<#FalseTrueFalseTrue#>Clear-Host 0, $null | ForEach {    ('#')*40    "`nTest `$null as default"    $null -gt $PSItem    $null -ge $PSItem    $null -eq $PSItem    $null -le $PSItem    $null -lt $PSItem    "`n"    ('#')*40    "Using $PSItem"    "`nTest `$null as string"    "Left Side`tRight Side`tBoth Sides"    Write-Host ([string]$null -gt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -gt [string]$PSItem) -NoNewline    Write-Host "`t|`t" ([string]$null -gt [string]$PSItem)    Write-Host ([string]$null -ge $PSItem) -NoNewline    Write-Host "`t|`t" ($null -ge [string]$PSItem) -NoNewline    Write-Host "`t|`t" ([string]$null -ge [string]$PSItem)    Write-Host ([string]$null -eq $PSItem) -NoNewline    Write-Host "`t|`t" ($null -eq [string]$PSItem) -NoNewline    Write-Host "`t|`t" ([string]$null -eq [string]$PSItem)    Write-Host ([string]$null -le $PSItem) -NoNewline    Write-Host "`t|`t" ($null -le [string]$PSItem) -NoNewline    Write-Host "`t|`t" ([string]$null -le [string]$PSItem)    Write-Host ([string]$null -lt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -lt [string]$PSItem) -NoNewline    Write-Host "`t|`t" ([string]$null -lt [string]$PSItem)    "`n"    ('#')*40    "Using $PSItem"    "`nTest `$null as boolean"    "Left Side`tRight Side`tBoth Sides"    Write-Host ([boolean]$null -gt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -gt [boolean]$PSItem) -NoNewline    Write-Host "`t|`t" ([boolean]$null -gt [boolean]$PSItem)    Write-Host ([boolean]$null -ge $PSItem) -NoNewline    Write-Host "`t|`t" ($null -ge [boolean]$PSItem) -NoNewline    Write-Host "`t|`t" ([boolean]$null -ge [boolean]$PSItem)    Write-Host ([boolean]$null -eq $PSItem) -NoNewline    Write-Host "`t|`t" ($null -eq [boolean]$PSItem) -NoNewline    Write-Host "`t|`t" ([boolean]$null -eq [boolean]$PSItem)    Write-Host ([boolean]$null -le $PSItem) -NoNewline    Write-Host "`t|`t" ($null -le [boolean]$PSItem) -NoNewline    Write-Host "`t|`t" ([boolean]$null -le [boolean]$PSItem)    Write-Host ([boolean]$null -lt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -lt [boolean]$PSItem) -NoNewline    Write-Host "`t|`t" ([boolean]$null -lt [boolean]$PSItem)    "`n"    ('#')*40    "Using $PSItem"    "`nTest `$null as int"    "Left Side`tRight Side`tBoth Sides"    Write-Host ([int]$null -gt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -gt [int]$PSItem) -NoNewline    Write-Host "`t|`t" ([int]$null -gt [int]$PSItem)    Write-Host ([int]$null -ge $PSItem) -NoNewline    Write-Host "`t|`t" ($null -ge [int]$PSItem) -NoNewline    Write-Host "`t|`t" ([int]$null -ge [int]$PSItem)    Write-Host ([int]$null -eq $PSItem) -NoNewline    Write-Host "`t|`t" ($null -eq [int]$PSItem) -NoNewline    Write-Host "`t|`t" ([int]$null -eq [int]$PSItem)    Write-Host ([int]$null -le $PSItem) -NoNewline    Write-Host "`t|`t" ($null -le [int]$PSItem) -NoNewline    Write-Host "`t|`t" ([int]$null -le [int]$PSItem)    Write-Host ([int]$null -lt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -lt [int]$PSItem) -NoNewline    Write-Host "`t|`t" ([int]$null -lt [int]$PSItem)    "`n"    ('#')*40    "Using $PSItem"    "`nTest `$null as double"    "Left Side`tRight Side`tBoth Sides"    Write-Host ([double]$null -gt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -gt [double]$PSItem) -NoNewline    Write-Host "`t|`t" ([double]$null -gt [double]$PSItem)    Write-Host ([double]$null -ge $PSItem) -NoNewline    Write-Host "`t|`t" ($null -ge [double]$PSItem) -NoNewline    Write-Host "`t|`t" ([double]$null -ge [double]$PSItem)    Write-Host ([double]$null -eq $PSItem) -NoNewline    Write-Host "`t|`t" ($null -eq [double]$PSItem) -NoNewline    Write-Host "`t|`t" ([double]$null -eq [double]$PSItem)    Write-Host ([double]$null -le $PSItem) -NoNewline    Write-Host "`t|`t" ($null -le [double]$PSItem) -NoNewline    Write-Host "`t|`t" ([double]$null -le [double]$PSItem)    Write-Host ([double]$null -lt $PSItem) -NoNewline    Write-Host "`t|`t" ($null -lt [double]$PSItem) -NoNewline    Write-Host "`t|`t" ([double]$null -lt [double]$PSItem)}# Results<#########################################Test $null as defaultFalseFalseFalseTrueTrue########################################Using 0Test $null as stringLeft Side   Right Side  Both SidesFalse   |    False  |    FalseFalse   |    False  |    FalseFalse   |    False  |    FalseTrue    |    True   |    TrueTrue    |    True   |    True########################################Using 0Test $null as booleanLeft Side   Right Side  Both SidesFalse   |    False  |    FalseTrue    |    False  |    TrueTrue    |    False  |    TrueTrue    |    True   |    TrueFalse   |    True   |    False########################################Using 0Test $null as intLeft Side   Right Side  Both SidesFalse   |    False  |    FalseTrue    |    False  |    TrueTrue    |    False  |    TrueTrue    |    True   |    TrueFalse   |    True   |    False########################################Using 0Test $null as doubleLeft Side   Right Side  Both SidesFalse   |    False  |    FalseTrue    |    False  |    TrueTrue    |    False  |    TrueTrue    |    True   |    TrueFalse   |    True   |    False########################################Test $null as defaultFalseTrueTrueTrueFalse########################################Using Test $null as stringLeft Side   Right Side  Both SidesTrue    |    False  |    FalseTrue    |    False  |    TrueFalse   |    False  |    TrueFalse   |    True   |    TrueFalse   |    True   |    False########################################Using Test $null as booleanLeft Side   Right Side  Both SidesTrue    |    False  |    FalseTrue    |    False  |    TrueFalse   |    False  |    TrueFalse   |    True   |    TrueFalse   |    True   |    False########################################Using Test $null as intLeft Side   Right Side  Both SidesTrue    |    False  |    FalseTrue    |    False  |    TrueFalse   |    False  |    TrueFalse   |    True   |    TrueFalse   |    True   |    False########################################Using Test $null as doubleLeft Side   Right Side  Both SidesTrue    |    False  |    FalseTrue    |    False  |    TrueFalse   |    False  |    TrueFalse   |    True   |    TrueFalse   |    True   |    False#>