Skip to content
elephantoo

Pythonic style, performance & next steps

Lesson 38 of 38 18 min read

PEP 8 and idioms, measuring and improving performance, maintainable code and career paths after this course.


Congratulations — you've covered the full journey from print("Hello") to web APIs, databases and packaging. This final lesson is about craft: writing code that other Python developers immediately recognise as clean and idiomatic ("Pythonic"), making it fast where it matters, and planning what to learn next on your way to a Python job.

The Zen of Python#

Type import this in the REPL:

Output
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
...

"Readability counts" is the heart of it. Code is read far more often than it's written — by teammates, reviewers and future you.

PEP 8 in a nutshell#

PEP 8 is the official style guide. The essentials:

  • Indentation: 4 spaces, never tabs.
  • Line length: 79 is the classic limit; many teams use 88–100. Be consistent.
  • Naming: snake_case for functions, variables and modules; PascalCase for classes; UPPER_SNAKE_CASE for constants; a leading _ for internal names.
  • Imports: at the top, one per line, grouped standard library / third-party / local.
  • Whitespace: spaces around operators and after commas (x = a + b, f(a, b)), none inside brackets; two blank lines between top-level functions and classes.
  • Comparisons: if x is None, if not items, if isinstance(x, int) — not == None, len(items) == 0 or type(x) == int.

Don't format by hand — let a tool do it. ruff checks style and common bugs, and ruff format (or black) rewrites your code consistently:

Terminal
pip install ruff
ruff check .          # lint: unused imports, undefined names, bad patterns...
ruff format .         # auto-format the whole project

Pythonic idioms#

Each pair below does the same job; the second version is what experienced Python developers write.

Python
items = ["pen", "ink", "pad"]
prices = [25, 60, 40]

# Looping with indexes
for i in range(len(items)):
    print(i, items[i])
# Pythonic
for i, item in enumerate(items):
    print(i, item)

# Parallel lists
for i in range(len(items)):
    print(items[i], prices[i])
# Pythonic
for item, price in zip(items, prices, strict=True):
    print(item, price)

More idioms worth adopting:

Python
from collections import Counter

words = "to be or not to be".split()
user = {"name": "Ada"}

# Build lists with comprehensions, not append-loops
lengths = [len(w) for w in words]

# Count with Counter, not manual dict bookkeeping
counts = Counter(words)

# Safe dict access with defaults
email = user.get("email", "unknown")

# Unpacking instead of indexing
first, *rest = words

# Truthiness for emptiness checks
if not rest:
    print("nothing else")

# Join strings instead of += in a loop
sentence = " ".join(words)

# Conditional expression for simple choices
label = "long" if len(words) > 5 else "short"

# any/all instead of flag variables
has_long_word = any(len(w) > 2 for w in words)

# Context managers for resources
with open("out.txt", "w", encoding="utf-8") as f:
    f.write(sentence)

print(lengths, counts.most_common(1), email, first, label, has_long_word)
Output
[2, 2, 2, 3, 2, 2] [('to', 2)] unknown to long True

A few more principles:

  • EAFP: try the operation and handle the exception, rather than checking every precondition first.
  • Return early with guard clauses instead of deep nesting.
  • Small functions with clear names beat comments explaining long ones.
  • Don't over-engineer: not everything needs a class; a function or a dict is often enough.
  • Use the standard library before writing helpers or adding dependencies.

Performance: measure first#

"Premature optimisation is the root of all evil." Write clear code first; then, if it's too slow, measure to find the real bottleneck. timeit compares small snippets:

Python
import timeit

setup = "data = list(range(10_000)); s = set(data)"
print(f"list lookup: {timeit.timeit('9_999 in data', setup=setup, number=2_000):.4f}s")
print(f"set lookup:  {timeit.timeit('9_999 in s', setup=setup, number=2_000):.4f}s")
Output
list lookup: 0.0...s
set lookup:  0.0000...s

For whole programs, use the built-in profiler to see which functions take the time:

Terminal
python3 -m cProfile -s cumulative my_script.py | head -20

The optimisations that actually matter

  1. Better algorithms and data structures. This dwarfs every micro-trick. Membership tests in a set or dict instead of a list turn O(n²) loops into O(n):
Python
import time

orders = list(range(20_000))
vip_ids_list = list(range(0, 20_000, 7))
vip_ids_set = set(vip_ids_list)

start = time.perf_counter()
slow = [o for o in orders if o in vip_ids_list]
t_list = time.perf_counter() - start

start = time.perf_counter()
fast = [o for o in orders if o in vip_ids_set]
t_set = time.perf_counter() - start

print(slow == fast, f"set version was {t_list / t_set:.0f}x faster")
Output
True set version was ...x faster
  1. Use built-ins and the standard library — sum, min, max, sorted, str.join, collections, itertools — they run in C.
  2. Vectorise numeric work with NumPy/pandas instead of Python loops.
  3. Avoid repeated work: hoist invariant computations out of loops, cache expensive pure functions with functools.cache.
  4. Stream large data with generators instead of building giant lists.
  5. Concurrency for I/O-bound waits (threads/asyncio) and processes for CPU-bound work.
  6. Upgrade Python: 3.11 was 10–60% faster than 3.10, and each release since has added more speed-ups.

Beyond that there are specialised tools — PyPy (a JIT-compiled Python), Cython, Numba, or writing hot paths in Rust with PyO3 — but most real-world slowness is fixed by points 1–5.

Writing maintainable code#

  • Type hints + mypy/pyright on public functions.
  • Tests for behaviour, run automatically in CI.
  • Docstrings for public modules, classes and functions; comments explain why, not what.
  • Logging, not print, in applications.
  • Configuration from the environment; no secrets in code.
  • Small, focused commits and pull requests with clear descriptions.

Where to go next#

You now have a complete foundation. Python careers branch in a few directions — pick one that excites you and build projects in it:

PathLearn nextBuild
Backend / web developerDjango or FastAPI in depth, SQL & PostgreSQL/MySQL, SQLAlchemy, authentication, Docker, REST design, Redis, Celerya full API with auth, a database, tests and deployment
Data analystpandas in depth, SQL, data visualisation (matplotlib, seaborn, Plotly), Jupyter, statistics, Excel/BI toolsan analysis of a public dataset, published as a notebook
Data engineerSQL, Polars/DuckDB, Airflow or Dagster, cloud storage, Spark basics, data modellinga scheduled pipeline that ingests, cleans and stores data
Machine learning / AINumPy, scikit-learn, PyTorch, LLM APIs and RAG, vector databases, evaluationa model or LLM-powered app with a FastAPI back-end
Automation / DevOps / QAscripting with pathlib/subprocess, APIs, Linux and Bash, CI/CD, Ansible, pytest + Playwrighttools that automate a real task at work or home

Some practical advice:

  • Build projects, not just tutorials. A to-do API, a web scraper that emails you price drops, a CLI that organises your downloads, a dashboard of your expenses. Put them on GitHub with a README and tests.
  • Read good code: the standard library source, popular projects like requests, rich or httpx.
  • Practice problem-solving on sites like LeetCode, HackerRank or Exercism for interview preparation — data structures and algorithms questions come up often.
  • Contribute to open source: start with documentation fixes or issues labelled "good first issue".
  • Keep up with releases: skim "What's New in Python 3.x" each October.
  • Explore Elephantoo's other courses — MySQL, Linux and JavaScript pair naturally with Python in almost every job.

A final checklist#

Before you call a Python project "done", check that it:

  • runs in a fresh virtual environment from pyproject.toml / requirements.txt
  • passes ruff check, ruff format --check and a type checker
  • has tests for its core behaviour, passing in CI
  • handles errors with specific exceptions and useful messages
  • logs instead of printing, and keeps secrets out of the code
  • has a README explaining what it does and how to run it

Common mistakes#

  • Optimising before measuring.
  • Clever one-liners that nobody (including you, next month) can read.
  • Ignoring linters and type checkers — they catch real bugs for free.
  • Endless tutorial-watching without building anything of your own.
  • Trying to learn every library — depth in the fundamentals beats shallow familiarity with everything.

What's next#

That's the end of the Python course — well done! Revisit any lesson's quiz to check your understanding, then start building. Happy coding! 🐘

Check your understanding

Quick quiz

0/3 answered
  1. 1.According to PEP 8, how should a function that calculates a total be named?

  2. 2.You need to check membership many times against 100,000 IDs. Which structure should hold the IDs?

  3. 3.What is the first step when your program is too slow?

Finished reading?

Mark this lesson complete to track your progress.