Python Best Practices
In this lesson: Write clean, readable Python — PEP 8 naming, functions, comments, errors and common mistakes to avoid. In Hindi & English.
Why clean code matters
Code is read far more often than it is written – by your teammates, by interviewers, and by you six months later. Clean code has fewer bugs, is easier to change and makes a strong impression in GitHub portfolios and machine-coding rounds.
Before and after: one program, two styles
import json
def f(d,t=[]):
for i in range(len(d)):
if d[i]['m']>=40 and d[i]['m']!=None:
t.append(d[i]['n'])
return t
x=json.loads('[{"n":"Asha","m":91},{"n":"Ravi","m":35}]')
print(f(x))
print(f(x))Look at the second line of output: the list grew to ["Asha", "Asha"] on the second call. That is the famous mutable default argument bug – t=[] is created once and shared by every call. The names f, d, t, m, n also hide what the code means.
"""Find students who passed an exam."""
import json
PASS_MARKS = 40
def passed_students(results: list[dict], pass_marks: int = PASS_MARKS) -> list[str]:
"""Return names of students whose marks are at least pass_marks."""
return [r["name"] for r in results if r.get("marks") is not None and r["marks"] >= pass_marks]
def main() -> None:
results = json.loads('[{"name": "Asha", "marks": 91}, {"name": "Ravi", "marks": 35}]')
print(passed_students(results))
print(passed_students(results))
if __name__ == "__main__":
main()| Change | Why |
|---|---|
Meaningful names (passed_students, results) | Code explains itself |
Constant PASS_MARKS | No “magic numbers”; one place to change |
| No mutable default; build a new list | Fixes the hidden bug |
Comprehension instead of range(len()) | Shorter and clearer |
is not None | Correct way to compare with None |
| Type hints and docstring | Editors and teammates know what goes in and out |
main() + if __name__ == "__main__" | File can be imported without running |
PEP 8 naming and layout
| Thing | Style | Example |
|---|---|---|
| Variable, function | snake_case | total_fees, send_email() |
| Class | PascalCase | StudentReport |
| Constant | UPPER_SNAKE | MAX_RETRIES = 3 |
| Private helper | leading underscore | _clean_name() |
| Indentation | 4 spaces, never tabs | |
| Line length | 79–100 characters (team choice) | |
| Imports | top of file: standard library, third-party, your modules |
Pythonic habits
students = ["Asha", "Ravi", "Neha"]
marks = [91, 58, 76]
# 1. enumerate instead of range(len())
for rank, name in enumerate(students, start=1):
print(rank, name)
# 2. zip to walk two lists together
report = {name: m for name, m in zip(students, marks)}
print(report)
# 3. f-strings with formatting
fee = 12500.5
print(f"Fee: ₹{fee:,.2f} | Pass rate: {2/3:.0%}")
# 4. truthiness instead of len(x) == 0
pending = []
if not pending:
print("No pending fees")
# 5. dict.get with a default
print(report.get("Kabir", "absent"))
# 6. unpacking
first, *others = students
print(first, others)Resources: always use with
# bad: file stays open if an error happens before close()
f = open("notes.txt", "w", encoding="utf-8")
f.write("Revise loops\n")
f.close()
# good: closed automatically, even on errors
with open("notes.txt", "a", encoding="utf-8") as f:
f.write("Practise functions\n")
with open("notes.txt", encoding="utf-8") as f:
print(f.read().strip())Handle errors specifically
def read_age(text):
try:
age = int(text)
except ValueError: # catch only what you expect
return None
if not 5 <= age <= 100:
raise ValueError(f"age {age} out of range")
return age
print(read_age("15"), read_age("fifteen"))
try:
read_age("150")
except ValueError as e:
print("Error:", e)except: or except Exception: pass hides real bugs (even typos and Ctrl+C). Catch the specific exception you can handle, and log or re-raise the rest.
Small functions with one job
- A function should do one thing and be explainable in one sentence.
- Prefer returning values over printing inside functions – returned values can be tested and reused.
- Avoid global variables; pass data in as arguments.
- More than 4–5 parameters? Group them in a dataclass or dict.
- Use keyword arguments for clarity:
send(to="riya", retry=True).
from dataclasses import dataclass
@dataclass
class Order:
item: str
price: float
qty: int = 1
@property
def total(self) -> float:
return self.price * self.qty
o = Order("Kaju Katli", 900, 2)
print(o)
print(o.total)Let tools check your style
| Tool | Job | Command |
|---|---|---|
| ruff | Very fast linter: finds unused imports, bugs, style issues | ruff check . |
| black / ruff format | Formats code automatically | ruff format . |
| mypy | Checks type hints | mypy app.py |
| pytest | Runs tests | pytest |
Running ruff on the “before” program finds problems automatically:
import json
def f(d,t=[]):
for i in range(len(d)):
if d[i]['m']>=40 and d[i]['m']!=None:
t.append(d[i]['n'])
return t
x=json.loads('[{"n":"Asha","m":91},{"n":"Ravi","m":35}]')
print(f(x))
print(f(x))Code review checklist
- Names explain purpose; no single letters except tiny loops.
- No duplicated code – repeated logic moved into a function.
- No magic numbers – named constants.
- Errors handled specifically; no bare except.
- Files and connections opened with
with. - Functions are short, return values, have docstrings for public use.
- Tests exist for the important rules.
- ruff shows no warnings; code is formatted.
Practice tasks
- Take one of your old programs and rename every variable to a meaningful name.
- Find and fix a mutable default argument in a function that collects items.
- Install ruff in a venv, run
ruff check .on a project and fix every warning. - Convert a dictionary-based record into a dataclass with a computed property.
Summary
- Follow PEP 8 names: snake_case, PascalCase, UPPER_SNAKE.
- Never use a mutable default argument such as
[]or{}. - Use enumerate, zip, f-strings, with, comprehensions and specific exceptions.
- Small functions, type hints, docstrings,
main()guard. - Automate style with ruff/black and correctness with pytest.
