The habits that make Python read like Python. None of this is about cleverness — it is about code someone else can pick up, which after a few months includes you.
PEP 8, enforced by a tool
PEP 8 is the style guide: four spaces, snake_case for functions and variables,
CapWords for classes, UPPER_CASE for constants, lines under about 88
characters.
Do not memorise it and do not argue about it. Install a formatter and let it decide:
pip install ruff
ruff format . # reformat everything
ruff check . # find problems
ruff check --fix . # fix the ones it can
Ruff does formatting and linting in one fast tool and has largely replaced black, flake8 and isort. Run it on save, or in a pre-commit hook. A team that formats automatically never has a style discussion again, which is the actual benefit.
Name things for what they are
# Hard to read
def calc(l, d):
return [x * (1 - d) for x in l]
# Easy to read
def apply_discount(prices, discount_rate):
return [price * (1 - discount_rate) for price in prices]
print(apply_discount([100.0, 250.0], 0.1)) # Output: [90.0, 225.0]
Both work. Only one tells you what a discount rate is. Single letters are fine for a coordinate or a
loop index and nowhere else — and l in particular is banned by every linter because it is
indistinguishable from 1 in many fonts.
Booleans read best as questions: is_active, has_overdraft,
can_withdraw. Functions that do something get a verb; functions that answer something get
a question.
Ask forgiveness, not permission
config = {"currency": "USD"}
# Look before you leap
if "overdraft" in config and isinstance(config["overdraft"], int):
limit = config["overdraft"]
else:
limit = 0
# Easier to ask forgiveness
try:
limit = int(config["overdraft"])
except (KeyError, TypeError, ValueError):
limit = 0
print(limit) # Output: 0
Python's convention is to attempt the operation and handle the failure, rather than testing every precondition first. It is shorter, and it has no gap between the check and the use — in the first version the value can change in between, and the check has to duplicate what the conversion already knows.
The check-first style is right when failure is expensive or the check is genuinely cheaper. For dictionary access and type conversion, try it.
Comprehension, then loop, then map
prices = [100.0, 250.0, 40.0]
print([p * 0.9 for p in prices]) # Output: [90.0, 225.0, 36.0]
print(list(map(lambda p: p * 0.9, prices))) # Output: [90.0, 225.0, 36.0]
print(sum(p for p in prices if p > 50)) # Output: 350.0
Use a comprehension when you are building one
collection from another. Use a plain loop when there is real logic, several statements, or anything you
would want to name. Reach for map and filter last — with a lambda they are
strictly longer than the comprehension.
And use a generator expression when the result is consumed immediately, as on the third line, so nothing is built just to be counted.
Use the standard library
from collections import Counter
from pathlib import Path
kinds = ["Deposit", "Withdrawal", "Deposit"]
print(Counter(kinds).most_common(1)) # Output: [('Deposit', 2)]
print(Path("a") / "b" / "c.txt") # Output: a/b/c.txt
Most "how do I count occurrences" code is Counter, most path manipulation is
pathlib, most grouping is defaultdict, and most CSV parsing is
csv. Before writing a helper, check whether collections,
itertools, functools or pathlib already has it. They usually
do, and theirs is tested.
Functions that do one thing
The practical test is whether you can name it without using "and". load_and_validate_and_save
is three functions. A function you can describe in one sentence is one you can test, reuse and read.
Return early rather than nesting — the guard-clause pattern from
Conditional Statements keeps the happy path at the
left margin. And keep functions honest about side effects: a function called
get_balance should not write to a file.
Comments explain why
balance = 100.0
# Bad: says what the code already says
balance = balance - 2.50 # subtract 2.50 from balance
# Good: says why
balance = balance - 2.50 # monthly fee, waived above $5,000 (see policy 4.2)
print(balance) # Output: 95.0
The code is the authority on what. A comment restating it is one more thing to keep in sync, and it will go stale. Spend comments on the reason, the constraint, the thing you tried that did not work, and the bug the odd-looking line is avoiding.
A docstring is different — it is the function's public description, and it belongs on anything a caller has to understand.
The things that cost real time
- Mutable default arguments.
def f(items=[])shares one list across every call. UseNone. See Functions. - Bare
except:swallows your own bugs and breaks Ctrl-C. Name the exception. - Floats for money.
0.1 + 0.2 != 0.3. UseDecimal, built from a string. - Mutating a list while iterating it. Build a new one.
- Files without
encoding="utf-8"— works on your machine, fails on someone else's. - Installing globally instead of in a virtual environment.
The tools worth setting up once
python3 -m venv .venv && source .venv/bin/activate
pip install ruff mypy pytest
pip freeze > requirements.txt
ruff check --fix . # style and common bugs
mypy . # type errors
pytest # behaviour
Three tools, three different classes of problem, all runnable in one line each. Add them to CI and they run on every change without anyone remembering to.
Above all: write the boring version. The clever one-liner you are proud of is the
line that confuses someone at 2am. Python's own guideline is that readability counts — type
import this for the rest of it.
Next: Code Snippets.