Clean Python code is code that a stranger can read and understand without asking you questions. The stranger might be a teammate, an open-source contributor, or you in six months after you have forgotten every detail of the implementation.
Python's syntax already encourages readability through mandatory indentation and English-like keywords, but readable syntax does not guarantee readable code. The practical habits in this article turn correct Python into clean Python. They build on the naming conventions from Name Python Variables, Functions, and Classes and the formatting rules from Follow the Python PEP 8 Style Guide.
Write small functions
A function should do one thing. If you cannot describe what a function does in a single sentence without using the word "and," the function does too much.
Consider this function that fetches, cleans, formats, and prints all at once. It is hard to test any single part of it without running the whole pipeline:
def process_and_display_users(url):
response = requests.get(url)
data = response.json()
cleaned = []
for user in data:
if user.get("active"):
cleaned.append({
"name": user["name"].strip().title(),
"email": user["email"].lower(),
})
for user in cleaned:
print(f"{user['name']} <{user['email']}>")Split it into focused functions. Each helper does exactly one job, and the main function orchestrates them in a few readable lines:
def fetch_users(url):
response = requests.get(url)
response.raise_for_status()
return response.json()
def filter_active(users):
return [u for u in users if u.get("active")]
def clean_user(raw):
return {
"name": raw["name"].strip().title(),
"email": raw["email"].lower(),
}
def display_users(users):
for user in users:
print(f"{user['name']} <{user['email']}>")
def main():
raw = fetch_users("https://api.example.com/users")
active = filter_active(raw)
cleaned = [clean_user(u) for u in active]
display_users(cleaned)Each function is independently testable. If the cleaning logic changes, only clean_user needs to be touched. The main function tells the whole story without requiring the reader to parse nested loops.
Prefer early returns over deep nesting
Deeply nested code is hard to follow because each level of indentation forces the reader to remember another condition. Flatten the structure by returning early for invalid or edge cases.
The first version below buries the happy path three levels deep behind three nested if statements. Reading it requires tracking which else pairs with which if:
def process_order(order):
if order is not None:
if order.is_paid:
if order.items:
total = sum(item.price for item in order.items)
ship_order(order, total)
return "shipped"
else:
return "no items"
else:
return "not paid"
else:
return "no order"The clean version uses guard clauses. Handle each edge case at the top with a return, then let the happy path sit at the bottom with zero nesting. The logic reads top to bottom with no surprises:
def process_order(order):
if order is None:
return "no order"
if not order.is_paid:
return "not paid"
if not order.items:
return "no items"
total = sum(item.price for item in order.items)
ship_order(order, total)
return "shipped"For more on structuring decisions cleanly, see writing conditional statements in Python.
Limit nesting to two levels
As a guideline, avoid more than two levels of indentation inside any function. If you see three or four levels, extract the inner block into its own helper. The helper isolates the nested logic and gives it a descriptive name.
A function with three levels of nesting forces the reader to track multiple conditions simultaneously. Extract the core iteration into a generator that yields duplicates as it finds them. The main function then becomes a single line that collects unique results:
def _collect_duplicates(users):
seen = set()
for user in users:
email = user.get("email", "").lower()
if not email:
continue
if email in seen:
yield email
else:
seen.add(email)
def find_duplicate_emails(users):
return list(dict.fromkeys(_collect_duplicates(users)))Avoid magic numbers and strings
A magic number is a literal value with no explanation. The reader has to guess what 30, 8, or 10 means in the context. Give these values descriptive constant names so the intent is self-documenting:
HIGH_TEMPERATURE_THRESHOLD = 30
MIN_PASSWORD_LENGTH = 8
REQUEST_TIMEOUT_SECONDS = 10
if temperature > HIGH_TEMPERATURE_THRESHOLD:
throttle_cpu()
if len(password) < MIN_PASSWORD_LENGTH:
raise ValueError("too short")
response = requests.get(url, timeout=REQUEST_TIMEOUT_SECONDS)Small well-known numbers like 0, 1, and -1 in obvious contexts do not need names. Nobody is confused by for i in range(3) or items[-1].
Use intermediate variables to name complex expressions
When an expression combines multiple operators and function calls, give each intermediate result a name. The variable names document what each sub-expression computes, and the final condition reads like a plain sentence:
days_since_login = (datetime.now(timezone.utc) - user.last_login).days
account_is_stale = days_since_login > 90
should_deactivate = account_is_stale and not user.is_admin
if should_deactivate:
deactivate(user)Compare this to the one-liner version where the reader must parse parentheses and operators before understanding the intent. The named version can be read aloud without any mental arithmetic.
Be consistent with data structures
If a function returns a list in one case and a dict in another, the caller must type-check the result before using it. Return the same shape every time, or raise an exception for the invalid case. A find function that returns a dict on success and raises ValueError on failure keeps the return type consistent:
def find_user(user_id):
for user in database:
if user.id == user_id:
return {"name": user.name, "email": user.email}
raise ValueError(f"User {user_id} not found")Keep related code together
Code that changes for the same reason should live in the same place. Group by feature, not by technical category. When users-related code lives in one directory, a user feature change touches only files in that directory. The alternative with scattered model, validator, and service directories forces you to touch three places for one change.
The readability test
After writing a function, read it aloud. If a sentence sounds awkward, rename the variable or restructure the logic. When you read if not order.is_paid aloud, it sounds like a sentence. When you read if order_status != 2, it sounds like a puzzle.
Clean code is not about perfection. Consistency in the small decisions lets the reader pattern-match instead of puzzling over each line. Apply these habits to new code as you write it, and every file becomes easier to work with.
Rune AI
Key Insights
- Write small functions that each do one thing and do it well.
- Use early returns to flatten conditional logic and reduce nesting.
- Choose descriptive names over clever shortcuts.
- Avoid more than two levels of nesting in any function.
- Write code as if the next person reading it knows where you live.
Frequently Asked Questions
What makes Python code clean?
How long should a Python function be?
Conclusion
Clean code is not about following rules for the sake of rules. It is about respecting the next person who reads your code, who is often you, six months from now. Write code that tells its story clearly, and the bugs that hide in confusing code will have fewer places to hide.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.