Write Clean and Readable Python Code

Learn practical techniques for writing Python code that is easy to read, understand, and maintain, from function design to variable naming and control flow clarity.

7 min read

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:

pythonpython
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:

pythonpython
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:

pythonpython
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:

pythonpython
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:

pythonpython
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:

pythonpython
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:

pythonpython
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:

pythonpython
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")

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

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.
RunePowered by Rune AI

Frequently Asked Questions

What makes Python code clean?

Clean Python code is code that another developer can read and understand in one pass. It uses small focused functions, descriptive names, flat control flow with early returns, and consistent formatting. The reader never needs to hold more than a few things in their head at once.

How long should a Python function be?

There is no hard limit, but most clean functions are under 20 lines. If a function does not fit on one screen, it probably does more than one thing. Split it until each function has a single clear responsibility.

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.