☰ Learn Python Tutorial Menu

Clean Code, Virtual Environments and Debugging

Written by CSA mentors · Updated 26 Sept 2026 · 6 min read


Will your script run on someone else's laptop? Will you understand it yourself in three months? Working code isn't the finish line. Code a colleague can read, run on a fresh machine and fix when it breaks is what turns a script from a liability into an asset, and it's what separates the candidates who get hired after our course from the ones who don't. This lesson collects those habits. Readable style, isolated environments, and a repeatable way to hunt down bugs.

Readable code, or PEP 8 on one page

PEP 8 is Python's official style guide. You don't need to memorise it. Follow these points and let an auto-formatter do the rest.

RuleDoAvoid
Namesmonthly_revenue, class InvoiceLine, GST_RATEmr, invoiceline, gstRate
Indentation4 spacesTabs, 2 spaces
Line lengthUp to 88 or 99 charactersLines you must scroll
Spacingtotal = a + b, f(x, y)total=a+b, f(x,y)
ImportsTop of file, standard library first, one per lineInside functions, import *
Comparisonsif not rows:, if x is None:if len(rows) == 0:, if x == None:
CommentsExplain whyRestate what the code obviously does
# Before
def f(d,r):
    t=0
    for x in d:
        if x[2]>r: t=t+x[2]
    return t

# After
def total_above(rows: list[tuple], threshold: float) -> float:
    """Sum the amount (third field) of every row above the threshold."""
    return sum(amount for _, _, amount in rows if amount > threshold)

Install black (formatter) and ruff (linter) locally with pip install black ruff, then run black report.py and ruff check report.py. They fix spacing and catch unused variables, undefined names and other slips before you run anything. We run both on every file in our own projects and have stopped arguing about formatting entirely.

Structure of a good script

"""Daily sales summary for the Lahore branch.

Reads exports/sales_YYYY-MM-DD.csv and writes summary_YYYY-MM-DD.txt.
"""
import csv
from datetime import date
from pathlib import Path

EXPORT_DIR = Path("exports")
BIG_ORDER = 20_000                       # underscores make large numbers readable


def load_rows(path: Path) -> list[dict]:
    with path.open(newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def summarise(rows: list[dict]) -> dict:
    amounts = [float(r["amount"]) for r in rows]
    return {"orders": len(rows), "revenue": sum(amounts),
            "big_orders": sum(a > BIG_ORDER for a in amounts)}


def main() -> None:
    today = date.today().isoformat()
    rows = load_rows(EXPORT_DIR / f"sales_{today}.csv")
    for key, value in summarise(rows).items():
        print(f"{key}: {value}")


if __name__ == "__main__":
    main()

Docstring at the top, then imports, constants, small functions with one job each, and a main() that reads like a summary. Anyone can tell what the script does in ten seconds, which is the test we apply when marking capstone projects.

Virtual environments

Two project folders each with its own .venv holding different package versions, both sharing one system Python

Different projects need different package versions. Install everything globally and one day an upgrade for project A breaks project B. A virtual environment is a private folder of packages for one project.

# create (once per project), inside the project folder
python -m venv .venv

# activate (every time you open a terminal)
.venv\Scripts\activate          # Windows
source .venv/bin/activate        # macOS / Linux

# install what the project needs
pip install pandas matplotlib

# record exact versions so others can reproduce your setup
pip freeze > requirements.txt

# on another machine
pip install -r requirements.txt

Add .venv/ to .gitignore and commit requirements.txt instead. When the prompt shows (.venv), python and pip point at the environment. Tools such as uv and poetry automate this further, but venv ships with Python and is enough to start.

Debugging

A five-step loop: read the traceback, reproduce with tiny input, inspect values, form a hypothesis, fix and re-run

1. Read the traceback properly

Traceback (most recent call last):
  File "report.py", line 31, in <module>
    main()
  File "report.py", line 27, in main
    rows = load_rows(EXPORT_DIR / f"sales_{today}.csv")
  File "report.py", line 14, in load_rows
    with path.open(newline="", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'exports/sales_2026-09-24.csv'

The bottom line gives the error type and message. Above it sits the chain of calls, innermost last. Here the fix is obvious (today's file doesn't exist yet), but the habit of reading bottom-up applies to every error you'll ever see.

2. Shrink the input

If a script fails on row 48,213 of a file, copy that row and two neighbours into a tiny test file. Bugs are found by narrowing, not by staring.

3. Look at real values

for row in rows:
    print(f"{row=}")                         # quick look, remove afterwards
    amount = float(row["amount"])

For anything beyond a quick look, put breakpoint() on the line before the failure. Python pauses there in the debugger. Type a variable name to see it, n for next line, c to continue, q to quit. Editors like VS Code offer the same with a click in the line margin.

4. Use logging instead of print in real scripts

import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)

log.info("Loaded %d rows", len(rows))
log.warning("Skipped row with bad amount: %s", row)

Logging can be switched off, sent to a file and timestamped, none of which print can do. A scheduled script should log to a file, so you can see what happened at 2 am without being awake at 2 am.

5. Write a small test

def test_summarise():
    rows = [{"amount": "100"}, {"amount": "30000"}]
    result = summarise(rows)
    assert result["orders"] == 2
    assert result["big_orders"] == 1

test_summarise()
print("ok")

An assert that fails raises AssertionError. Save tests in test_report.py, run them with pytest (pip install pytest), and every future change gets checked in seconds. Tests are the difference between "I think it still works" and "it works".

Common bugs and their fingerprints

SymptomLikely cause
Result is NoneFunction missing return, or x = list.sort()
Totals wildly too largeAdding strings ("2500" + "1800")
Loop runs once or neverWrong indentation, or iterating a generator twice
Changes appear in another listAliasing; missing .copy()
KeyError on a column that existsTrailing space or case in the header, e.g. "Amount "
Works on your PC, fails on the serverMissing package (no requirements.txt) or hard-coded path

Right on the laptop, wrong on the server: A JazzCash analytics intern's report was correct on her machine and wrong on the shared server. The server had an older pandas where a default argument differed. Pinning versions in requirements.txt and creating a virtual environment on the server fixed it. She also added a three-line test that checks a known day's total, which now runs before every scheduled execution and has caught two data-format changes since.

Habits to keep

  • Follow PEP 8 with descriptive names, small functions and a main(), and let black and ruff enforce it.
  • One virtual environment per project, with packages pinned in requirements.txt.
  • Read tracebacks bottom-up, shrink the failing input, and inspect real values with {x=} or breakpoint().
  • Use logging in scheduled scripts and write small assert tests for core functions.
  • Most "mysterious" bugs are types, aliasing, indentation or environment differences.

These habits are also most of what the practical assessment for the CSA certification looks for. Clean structure, a requirements file and one test will put you ahead of most submissions.

Do these this week

  1. Take any script you wrote earlier in this track and restructure it into functions with a main() guard, type hints and a module docstring.
  2. Locally, create a virtual environment, install pandas, freeze requirements, delete the environment, and recreate it from the file.
  3. Introduce a deliberate bug (a string amount) into summarise(), run the test, read the traceback, and fix it using breakpoint() at least once.

Lesson 25 of 26

Sign in to track your progress and earn learning points for every lesson you finish.

Example

print("Hello from CSA!")

for i in range(5):
    print(i * i)
Try it Yourself »