Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 8 of 10

Code that runs is the low bar. Pythonic code is code another Python programmer reads once and understands. This lesson is about that gap.

The Zen of Python

Python ships with its own design philosophy. Run this in any Python:

import this

The lines that matter most day to day:

  • Readability counts.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • There should be one — and preferably only one — obvious way to do it.

That last one explains why Python has so many idioms: the community converges on one way, and everybody learns to read it fast.

PEP 8, in depth

PEP 8 is the official style guide. You met it briefly in lesson 1.6; here's the working set.

Naming

| Thing | Style | Example | | --- | --- | --- | | Variable, function | snake_case | total_score, send_email() | | Class | PascalCase | BankAccount | | Constant | UPPER_SNAKE | MAX_RETRIES | | "Internal" | leading _ | _cache | | Module / file | short lowercase | utils.py |

Never name something l, O, or I — in most fonts they're indistinguishable from 1 and 0. And never shadow a built-in: naming a variable list, dict, str, sum, or id breaks the real one for the rest of the scope.

Layout

  • 4 spaces per indent level. Never tabs, never 2.
  • Max line length 79 (PEP 8) or 88 (what most modern tools use). Pick one and stay consistent.
  • Two blank lines between top-level functions and classes, one between methods.
  • Imports at the top, one per line, grouped: standard library, then third-party, then your own — with a blank line between groups.
import json
import os

# third-party imports would go here, e.g. requests

from mypackage import helpers

Whitespace

# Good
total = price * quantity
values = [1, 2, 3]
result = my_function(a, b)


def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

Spaces around operators, after commas, none just inside brackets, and none around = for keyword arguments (greeting="Hello", not greeting = "Hello").

Let a tool do it

Nobody formats by hand any more. black or ruff format rewrites your file to a consistent style; ruff or flake8 flags problems. Install them with pip (lesson 7.4) and stop thinking about spacing forever.

The idioms

This is the part that actually marks you as fluent.

Loop over items, not indices

names = ["ada", "grace"]

# Not Pythonic
for i in range(len(names)):
    print(names[i])

# Pythonic
for name in names:
    print(name)

Need the index too? enumerate, not a manual counter:

for i, name in enumerate(["ada", "grace"], start=1):
    print(i, name)

Walk two sequences with zip

names = ["ada", "grace"]
scores = [95, 88]

for name, score in zip(names, scores):
    print(name, score)

Use truthiness

items = []

# Not Pythonic
if len(items) == 0:
    print("empty")

# Pythonic
if not items:
    print("empty")

Empty containers, 0, "", and None are all falsy. But compare to None with is, never with truthiness — 0 and None are both falsy and mean very different things:

value = 0

print(value is None)
print(not value)

Unpack instead of indexing

point = (3, 4)

x, y = point
print(x, y)

first, *rest = [1, 2, 3, 4]
print(first, rest)

a, b = 1, 2
a, b = b, a
print(a, b)

Build strings with join

words = ["evil", "genius", "society"]

print(" ".join(words))
print("-".join(words))

Looping with += on a string builds a new string every time. join does it in one pass.

Comprehensions over append loops

numbers = [1, 2, 3, 4, 5]

squares = [n ** 2 for n in numbers if n % 2 == 1]
print(squares)

But stop at one condition and one loop. Past that, a real for loop reads better — see lesson 9.1.

dict.get instead of key-checking

config = {"debug": True}

print(config.get("host", "localhost"))

Chained comparisons

age = 25

print(18 <= age < 65)

any and all

scores = [95, 88, 91]

print(all(s >= 60 for s in scores))
print(any(s > 90 for s in scores))

Both short-circuit — they stop at the first answer that decides it.

Ask forgiveness, not permission

Python prefers EAFP (easier to ask forgiveness than permission) over LBYL (look before you leap):

data = {"count": "12"}

# LBYL
if "count" in data and data["count"].isdigit():
    print(int(data["count"]))

# EAFP
try:
    print(int(data["count"]))
except (KeyError, ValueError):
    print(0)

EAFP wins when the happy path is the common case: no double lookup, and no race between the check and the use.

with for anything that needs closing

Files, locks, connections. Never a bare open() you have to remember to close — lesson 6.2.

Comments and docstrings

Comment why, not what. The code already says what.

# Bad: increment the counter
# Good: users can retry twice before we lock the account

Every public function, class, and module gets a docstring (lesson 4.9). Anything with a non-obvious reason gets a comment. Nothing else does — a comment that repeats the code is one more thing to keep in sync, and it will drift.

Try it yourself

Every line below is an idiom worth memorising.

Rewrite this

Here's clumsy code and its Pythonic equivalent. Run both — same output, very different reading experience.

Checkpoint

  • PEP 8 covers naming, 4-space indents, import grouping, and whitespace — let black or ruff enforce it.
  • Loop over items; enumerate for the index, zip for two sequences.
  • Use truthiness for emptiness, but is None for None.
  • join to build strings, comprehensions for simple transforms, dict.get for defaults.
  • any / all for "is there one" / "are they all", chained comparisons for ranges.
  • EAFP over LBYL: try it and handle the exception.
  • Comment why, docstring every public thing, delete everything else.

Questions about this lesson

Log in to ask a question.