Skip to content

Logical Operators

Operator Description C# Mapping
and Logical AND (short-circuit) &&
or Logical OR (short-circuit) \|\|
not Logical NOT !

Truthiness

Operands of and, or, and not are evaluated for truthiness — the same rule that governs if, while, assert, ternary conditions, comprehension filters, and match guards (an arm guard case p if g: and a per-alternative guard pattern (p if g) alike). A type is truth-testable when it has a falsy case:

Type Truthiness lowering Falsy value
bool identity False
int x != 0 0
float x != 0.0 0.0
long x != 0L 0L
str x.Length > 0 ""
bytes ((ISized)x).Count > 0 b""
collections (list, dict, set, and any type whose CLR shape carries ISized — the collections mappings defaultdict, OrderedDict, ChainMap, Counter, the dict views) ((ISized)x).Count > 0 empty
None false (always) None
T? (strict Optional) x.IsSome None()
T \| None (loose nullable) x != null None
UDT with __bool__ x.IsTrue implementation-defined
UDT with __len__ ((ISized)x).Count > 0 empty
conditional (x if c else y) distributed: c ? truth(x) : truth(y) per-branch
fixed-arity tuple[T1, ..., Tn] refused (SPY0220) no falsy case — every value of a given tuple type has the same, statically-known truthiness
objects, functions, delegates refused (SPY0220) no falsy case

Deviation from Python: Python makes objects without __bool__/__len__ vacuously truthy. Sharpy refuses them — the check can never do anything useful.

Deviation from Python (tuple truthiness): every non-empty Python tuple is truthy and () is falsy — bool()/len()-based, like any other sequence. A fixed-arity tuple[T1, ..., Tn] in Sharpy is refused instead: its arity is part of its TYPE, so every value of that type has the SAME truthiness (always truthy for a non-empty arity, always falsy for the zero-arity tuple[] the () literal spells) — testing it is never a runtime question, only a compile-time constant. Test len(t) or a specific element instead. A tuple reached through a loose tuple[...] | None or a strict tuple[...]? wrapper is unaffected — the wrapper's own truthiness (null-check / is-some) is tested, never the tuple's.

The refusal is not uniform across every use of the word "truthiness": it covers the sixteen truth-testing positions — if, while, assert, and/or/not, ternary conditions, comprehension filters, match guards and the rest of the list above — where a tuple operand is a compile-time constant masquerading as a runtime test. An explicit bool(t) call is a conversion, not a truth-testing position, and is admitted, as in Python:

def main() -> None:
    t = (1, "a")
    print(bool(t))      # admitted — bool() is not a truth-testing position
    print(len(t))       # 2 — the arity, the thing worth testing
    print(bool(()))     # False — the zero-arity tuple is falsy, like any empty sequence

Output:

True
2
False

bool(t) answers from the arity, so it is False for the zero-arity tuple[] that () spells and True for every other arity, matching Python.

A conditional expression in a truthiness position distributes the test per branch (TruthinessLowering.Distributed). Each branch is tested for truthiness independently, so the branches need not share a common type. Distribution recurses: a conditional nested inside a distributed branch is itself in a truthiness position and needs no type either.

class Yes:
    def __bool__(self) -> bool:
        return False

class No:
    def __bool__(self) -> bool:
        return True

def main() -> None:
    flag = True
    g = False
    if Yes() if flag else No():          # Yes and No share no common type
        print("truthy")
    else:
        print("falsy")
    if ("a" if g else 0) if flag else 1:  # the nested ternary distributes too
        print("nested-truthy")
    else:
        print("nested-falsy")

Output:

falsy
nested-falsy

Return Type

and and or return bool, not the operand value. This is a deliberate deviation from Python, where "" or "Anonymous" returns "Anonymous". In Sharpy, "" or "Anonymous" evaluates to True. See null_coalescing_operator.md for the ?? operator that provides the Python-style fallback behavior.

Assigning the result of or/and to a non-bool slot is refused with a steer naming the rewrite:

name: str | None = None
fallback: str = name or "Anonymous"  # error: Cannot assign type 'bool' ... use '??'

For nullable or optional left operands, ?? is the Python-style fallback:

name: str | None = None
fallback: str = name ?? "Anonymous"  # "Anonymous"
print(fallback)

Examples

x: int = 42
if x and x > 0:
    print("positive")

name: str = ""
result: bool = name or "fallback"  # True (both sides truth-tested)

xs: list[int] = [1, 2, 3]
print(not xs)  # False (non-empty list is truthy)