| name | explicit-primitive-conversion |
| description | Use when converting a value to int, float, str, or bool in Python โ about to write int(x), float(x), str(x), bool(x), a bare `if x:` / `x or default` on a value where None, 0, or "" mean different things, or filter(None, xs). |
Explicit Primitive Conversion
Overview
Blanket conversions hide intent and bury edge cases. bool(x) and a bare if x: collapse None, 0, 0.0, "", [], {}, and False into one bucket; str(x) quietly turns None into "None" and objects into repr noise; int(f) silently truncates toward zero; a naked int(s) on external input is an unhandled ValueError three frames away. Convert explicitly, and handle each edge case by name.
bool(x) for domain logic, x or default on a value with meaningful falsy states, and naked int()/float() on external strings are banned. No exceptions.
The rule
| Goal | โ banned (implicit) | โ
required (explicit) |
|---|
| external string โ int/float | naked int(x) / float(x), int(x or 0) | int(x) in a try/except ValueError that names the failure โ or a pydantic model / Result (see chaining-returns-results) |
| float โ int | int(f) (silent truncate-toward-zero) | name the rounding: math.floor(f) / math.ceil(f) / round(f) |
| anything โ str | str(x) / f"{x}" on an Optional or object-typed value | handle None first: "" if x is None else str(x) โ or raise |
| presence / emptiness / zero | bool(x), bare if x: where None/0/"" differ, x or default | x is None / x == "" / count == 0 / len(xs) == 0; default if x is None else x |
| drop falsy items | filter(None, xs), [x for x in xs if x] | name the condition: [x for x in xs if x is not None] |
Still allowed: str(port) on a known int โ the type and rendering are what you intend; if not items: on a plain, never-None collection where emptiness is the whole question; comparisons themselves โ ok = x is not None already is a bool, there is nothing left to coerce.
Before โ after
page = int(query.get("page") or 1)
label = f"user: {user.nickname}"
apply = bool(user) and bool(rate)
tags = [t for t in raw if t]
percent = int(ratio * 100)
raw_page = query.get("page")
page = 1 if raw_page is None else parse_page(raw_page)
label = f"user: {user.nickname}" if user.nickname is not None else "user: (unset)"
apply = user is not None and rate > 0
tags = [t for t in raw if t is not None and t != ""]
percent = math.floor(ratio * 100)
Why bare truthiness is the one that bites
if x: / bool(x) / x or default test "is it truthy" โ but you almost never mean all seven falsy values. Writing the explicit condition forces you to name the one you actually mean:
- "is it present?" โ
x is not None (if x: would also reject a legitimate 0 or "")
- "is the string non-empty?" โ
x != ""
- "is the count positive?" โ
count > 0 (x or default would have replaced a real 0)
x or default is the trap that passes review: it reads as "default if missing" but means "default if falsy", so a real 0 or "" silently becomes the default. Write default if x is None else x.
A value typed Any (or untyped)?
Do not reach for str(x) / int(x) to dodge the type. Model it first (see precise-type-modeling) โ a pydantic model at the boundary converts once, validates, and names every failure. f"{x}" on a bare Any is itself a smell that the type was never modeled; don't launder it through a conversion.
Common mistakes
| Mistake | Fix |
|---|
int(x or 0) "to handle missing input" | Collapses None, "", and "0" into one case. Branch on x is None, then parse with error handling. |
int(f) "to round" | It truncates toward zero (int(-1.5) == -1). Say which rounding you mean: math.floor / math.ceil / round. |
if x: (or x == None) for presence | x is None / x is not None โ identity, immune to __bool__/__eq__. |
if x: to detect NaN | float("nan") is truthy (and != itself). Use math.isnan(x). |
bool(flag_count) "because I need a bool" | You need a condition: flag_count > 0. A comparison already returns bool. |
Naked int(s) "pyright didn't complain" | Type checkers can't see that s came from the network. The ValueError is yours to name โ or validate with pydantic. |
Red Flags โ STOP
- About to write
bool( โ truthiness collapse. Write the is None / == / > condition you mean.
- About to write
if x: or x or default on an Optional, an int/float, or a str where "" is valid data โ name the condition.
- About to write
filter(None, ...) or if t in a comprehension โ name the predicate.
- About to write
int(x) / float(x) on anything from outside the process โ named error handling or a pydantic validator.
- About to write
str(x) / f"{x}" where x can be None or an arbitrary object โ handle the None/repr case first.
Ruff catches fragments of this (SIM truthiness rules); this skill exists for the part it can't โ choosing the explicit replacement that says what you mean.