Chapter 24 of 24 · Python for Beginners — Complete Free Tutorial

Python Best Practices and Where to Go Next

In the Python for Beginners series you have come a long way — from your first variable to decorators, type hints, tests, asyncio and the standard library. This final chapter of the 24-chapter course steps back from new features and does two jobs: it collects the habits that separate code that merely runs from code you will still be happy to open next year, and it points at where to go once this series ends.

What to prefer

Most “Pythonic” style advice is really advice about letting the language do the bookkeeping. Iterate instead of indexing, build results with comprehensions instead of append scaffolding, and unpack instead of reaching into tuples with [0]. The table below lists the swaps that pay off most often:

Instead of…Prefer…Why it reads better
for i in range(len(items)):for item in items:No off-by-one, no index you never use.
Loop and .append() into a listA list comprehensionStates the transformation in one line.
Sentinel values like -1None plus an explicit checkHonest about “nothing here”.
try/except: passCatching the exact exceptionFailures stay visible instead of vanishing.
Manual counters and flagsenumerate, zip, anyStandard library intent, not plumbing.

The comprehension swap is worth seeing side by side — both functions below do exactly the same job:

names = ["ada", "grace", "linus"]

def shout_all(words):
    result = []
    for word in words:
        result.append(word.upper() + "!")
    return result

print(shout_all(names))

def shout_all(words):
    return [word.upper() + "!" for word in words]

print(shout_all(names))

Expected output:

['ADA!', 'GRACE!', 'LINUS!']
['ADA!', 'GRACE!', 'LINUS!']

What to avoid

Just as important are the traps that compile happily and fail later. A bare except: swallows KeyboardInterrupt along with your bugs. Importing a module with from x import * pollutes your namespace with names nobody can trace. Mutable module-level state makes order of execution matter. And the most famous one of all — a default argument that is created once, when the def runs, and then shared by every call:

def remember(item, bucket=[]):
    bucket.append(item)
    return bucket


print(remember("a"))
print(remember("b"))

Expected output:

['a']
['a', 'b']

The second call should have printed ['b']; instead the list from the first call is still lurking. The fix is the pattern you met in the error-handling chapters: default to None and create the real value inside the function.

Measure, then optimise

Python style also means resisting premature optimisation. A comprehension is usually faster than a loop, but the honest rule is: profile before you rewrite. The profiler built into the standard library needs no install — given a small script such as this one:

def busy_loop(n):
    total = 0
    for i in range(n):
        total += i
    return total


def report():
    return sum(busy_loop(200_000) for _ in range(5))


print(report())

you can run it under cProfile and sort by cumulative time:

python3 -m cProfile -s cumtime slow_demo.py

Expected output: (timings vary from run to run)

99999500000
         17 function calls in 0.027 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.027    0.027 {built-in method builtins.exec}
        1    0.000    0.000    0.027    0.027 slow_demo.py:1(<module>)
        1    0.000    0.000    0.027    0.027 slow_demo.py:8(report)
        1    0.000    0.000    0.027    0.027 {built-in method builtins.sum}
        6    0.000    0.000    0.027    0.005 slow_demo.py:9(<genexpr>)
        5    0.027    0.005    0.027    0.005 slow_demo.py:1(busy_loop)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}

One glance shows all the time sitting in busy_loop — the machine has told you where a rewrite would matter, and that every other line is already fast enough.

Your tooling checklist

Good habits stick when a tool enforces them instead of your memory. This five-tool setup covers formatting, style, types, tests and enforcement, and each one is a single command away inside your project’s virtual environment:

ToolJobTypical command
ruffLinting — catches dead code, unused imports and smellsruff check .
blackFormatting — one style, zero debate in reviewblack .
mypyType checking — your annotations, verifiedmypy src/
pytestTesting — the suite you built in chapter 21pytest -q
pre-commitEnforcement — runs all of the above before every commitpre-commit install

You do not need all five on day one. Start with pytest and ruff, add mypy once you have annotations worth checking, and let black end any argument about spacing forever.

Where to go next

The best way to keep learning is to build something that needs a skill you do not have yet. A command-line tool with real argparse flags, a web API with FastAPI (where your type hints become automatic documentation), or a data script with pandas are all natural next projects. And because Scriptly teaches the rest of the stack too, two tracks pair particularly well with what you now know: if you want to see how the server side of the web looks in another runtime, start Introduction to Node.js; if you want statically checked types in the browser, Introduction to TypeScript will feel familiar — the annotation habits from chapter 20 transfer directly.

Common mistakes

  • Optimising from a hunch — rewriting loops you merely suspect are slow. Run cProfile first; it will usually point somewhere you did not expect.
  • Bare except: everywhere — it hides bugs and catches KeyboardInterrupt. Catch the narrowest exception you can actually handle.
  • Trusting the mutable default — a default list or dict is created once and shared by every call. Default to None and build inside the function.
  • Skipping the virtual environment — installing packages globally works until it clashes with another project. One venv per project, no exceptions.
  • Waiting for the “right time” to add tests — retrofitting a suite onto legacy code is the hardest version of testing. Add tests with each new feature while the intent is fresh.

Practice checklist

  1. Take your oldest script and apply this chapter’s table: replace index loops with iteration and rebuild one loop as a comprehension.
  2. Find (or write) a function with a mutable default argument, reproduce the shared-state bug, and fix it with the None pattern.
  3. Profile one of your own scripts with python3 -m cProfile -s cumtime and identify the single hottest function.
  4. Set up ruff and black in a project and fix everything they flag on the first run.
  5. Pick a small idea — a notes CLI, a budget tracker, a quiz game — and build it end to end with type hints, pytest coverage and a README.

What's next?

Congratulations — you have finished all 24 chapters of Python for Beginners, from your first print() to decorators, tests and async code. Keep the momentum going on the tutorials index, where the other language tracks all build on habits you now share, and round out your perspective on writing maintainable code in TypeScript Best Practices and Where to Go Next.

Quiz: Python Best Practices and Where to Go Next

5 questions · 70% to pass

Frequently Asked Questions

Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.

None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.

Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.

More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.

Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.

Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.

A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.

Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.

Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.

JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.