These are the habits that separate Python that works from Python you can return to in six months. None is difficult. Most take a few seconds once they are automatic, and together they remove a large share of the bugs and confusion beginners run into.
Naming#
# Good
user_count = 10
def calculate_total(items): ...
class ShoppingCart: ...
MAX_RETRIES = 3
# Avoid
uc = 10
def calcTot(i): ...
class shopping_cart: ...
Snake case for variables and functions, capitalised words for classes, upper case for constants. Beyond convention, names should say what the thing is: days_until_expiry needs no comment; d needs a paragraph.
Never name a file or variable after something in the standard library. A file called random.py or csv.py shadows the real module, and the resulting errors are baffling.
Let a formatter do the formatting#
python -m pip install ruff
ruff format . # formats
ruff check . --fix # finds problems and fixes what it can
The value is not that any particular style is better — it is that formatting stops being a decision. Set it to run on save and you never think about line breaks again.
Functions should return, not print#
# Hard to reuse or test
def show_average(numbers):
print(sum(numbers) / len(numbers))
# Easy to do both
def average(numbers):
if not numbers:
return 0
return sum(numbers) / len(numbers)
print(average(scores))
The second version can be tested in one line, used inside another calculation, and its output formatted differently in different places. Print at the edges of your program; return everywhere else.
Keep functions to one job. If the name needs an “and” in it, that is two functions.
Handle specific exceptions#
# Hides everything, including your own typos
try:
value = int(user_input)
except:
value = 0
# Says what you expected to go wrong
try:
value = int(user_input)
except ValueError:
value = 0
A bare except catches KeyboardInterrupt and SystemExit too, so it can make a program impossible to stop with Ctrl+C. It also swallows the NameError from a misspelled variable, which turns a five-second fix into an hour of confusion.
try:
data = json.loads(text)
except json.JSONDecodeError as error:
log.error("Bad JSON at position %s", error.pos)
raise # re-raise if you cannot actually handle it
One virtual environment per project#
python -m venv .venv
source .venv/bin/activate # .venv\Scripts\activate on Windows
python -m pip install requests
python -m pip freeze > requirements.txt
Always python -m pip rather than bare pip — it guarantees the install goes to the interpreter you just ran, which prevents most “it says it is installed but the import fails” problems.
Add .venv/ and __pycache__/ to .gitignore. Commit requirements.txt.
Keep secrets out of your code#
import os
# Never
API_KEY = "sk-1234567890abcdef"
# Instead
API_KEY = os.environ["API_KEY"] # required
DEBUG = os.environ.get("DEBUG", "false") == "true" # optional
Using os.environ["..."] for required values means the program fails immediately with a clear message rather than making a request with an empty key and getting a confusing 401.
Type hints#
def average(numbers: list[float]) -> float:
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
def find_user(user_id: int) -> dict | None:
...
Python ignores them at run time, so they cost nothing. What they buy you is real: your editor can autocomplete and catch mistakes, and a checker such as mypy finds a class of bug before you run anything.
python -m pip install mypy
mypy .
Add them to new code as you write it. Retrofitting a whole codebase at once is rarely worth the effort.
Prefer the idiom#
# Iterating
for i in range(len(items)): # C-style
print(items[i])
for item in items: # Pythonic
print(item)
for i, item in enumerate(items): # when you need the index
print(i, item)
# Building a list
result = []
for n in numbers:
if n > 0:
result.append(n * 2)
result = [n * 2 for n in numbers if n > 0] # clearer
# Checking emptiness
if len(items) == 0: ...
if not items: ... # clearer
# Opening files
f = open("data.txt")
data = f.read()
f.close() # not closed if read() raises
with open("data.txt") as f: # always closed
data = f.read()
Write one test#
# tests/test_average.py
from mymodule import average
def test_average_of_numbers():
assert average([2, 4, 6]) == 4
def test_average_of_empty_list():
assert average([]) == 0
python -m pytest
Two tests on the function most likely to break are worth far more than none. The empty-list case above is exactly the kind of thing that quietly stops working when someone refactors.
Comment the why#
# Adds one to i
i += 1
# Skip the header row - this supplier includes it in every export
i += 1
The code already says what it does. A comment earns its place by explaining why — a workaround, a business rule, a decision that looks wrong until you know the context.
Docstrings on anything non-obvious:
def calculate_discount(price: float, tier: str) -> float:
"""Return the discounted price for a customer tier.
Unknown tiers get no discount rather than raising, because
supplier feeds occasionally contain tiers we have not seen.
"""
Project structure#
myproject/
.venv/
.gitignore
README.md
requirements.txt
pyproject.toml
myproject/
__init__.py
core.py
cli.py
tests/
test_core.py
Run things with python -m myproject.cli from the root. That keeps the project directory on the import path and avoids most import errors.
Questions people ask#
What is PEP 8?
Python’s official style guide — naming, indentation, line length, import order. You do not need to memorise it; a formatter applies it for you.
Are type hints worth it on small scripts?
On a twenty-line script, not really. On anything with functions other code calls, yes — the editor support alone repays the effort.
How much test coverage should I aim for?
Not a number. Test the parts where a bug would cost you the most, and anything you have already had to fix once. High coverage of trivial code is a poor use of time.
Should I use f-strings everywhere?
For building strings, yes — they are clearer and faster. The exception is logging, where log.info("User %s", name) lets the logger skip formatting entirely when that level is disabled.
Where to go next#
- Building a complete Python project — the structure section, in full.
- Python virtual environments — the single most valuable habit here.
- Python try/except explained — specific exceptions in depth.