Skip to content

NAM103: a predicate that doesn't return a bool

Junior

A name that starts with is_, has_, can_, should_, was_ or check_ asks a yes-or-no question. Callers write if is_ready(): and expect True or False.

def is_admin(user) -> str:  # NAM103: returns a string
    return user.role


def is_admin(user) -> bool:  # answers the question
    return user.role == "admin"

When a predicate returns something else, such as a string or a number, Python still treats it as true or false. An empty string and zero count as false. Every other value counts as true. So the code works until the day the value is empty.

To fix it, return a bool. If the function really returns a value, rename it after the value: role_of(user), admin_role(user).

Senior

The bug from a predicate that returns a value shows up in a caller, far from the function, and only for one input: the empty string, zero, the empty list. That is what makes it worth a check.

namelint reads the return annotation when there is one, and the return statements when there isn't. It never infers types, so when it can't tell, it stays quiet:

  • return self._compute() could return anything.
  • -> Any and -> object say the author doesn't know either.
  • -> TypeGuard[X] and -> TypeIs[X] are bools.
  • return None is an early exit in most predicates.
  • A check_ annotated -> None reports a problem by raising, which is a valid way to answer.

A name whose last word names the returned type is a factory, and stays quiet: is_instance_schema(cls) -> IsInstanceSchema builds a schema. Whole words only, so is_valid() -> ValidationResult is still reported.

A check_ that returns a result object is a common case where the right fix is the name: run_check, dns_check, spf_record.

Expert

The annotation is read first, string forward references included. It is reported, unparsed, as the evidence unless it is bool, a TypeGuard[...] or TypeIs[...], or mentions Any or object anywhere: -> dict[str, Any] | bool stays quiet. Without an annotation, the first return whose value is evidently not a bool is the evidence: a non-bool constant, an f-string, a container, a lambda. Calls, names and attributes are unknown and stay quiet.

A generator under a predicate name is NAM107's finding and is skipped here. A quoted return value is cut at 40 characters in the message.

check_ joined the predicate prefixes in 0.2.0. Since namelint must pass its own lint, its own checks are named find_*.

This is the quietest check in the corpus: 10 findings in CPython's 51,726 functions, and one in pytest. In the first measured run both findings were genuine.