Python Best Practices: Writing Pythonic Code

What "Pythonic" means in practice, as before-and-after pairs you can run: looping with enumerate and zip, unpacking, dictionary and Counter idioms, building strings, checking None and emptiness, handling files, plus naming, function design and the formatter, linter and type checker that keep code consistent.

GloryTecks Team

Training & Placement Faculty

6 May 2026 7 min read

Last updated: 24 September 2026

Share:

"Pythonic" is not a synonym for "short". It means using the constructs Python was designed around, so that someone who knows the language can read your code without translating it. The Zen of Python puts the goal in one line: readability counts (PEP 20). Most unpythonic code is a habit carried over from another language: C-style index loops, Java-style getters, manual counting. The fix is usually a built-in you have not met yet. This guide shows the most useful idioms as before-and-after pairs, each of which runs as shown.

Loop over things, not over indexes

python
names = ["Asha", "Ravi", "Meena"]
scores = [91, 78, 85]

# Before: index juggling
for i in range(len(names)):
    print(i + 1, names[i], scores[i])

# After: enumerate for the counter, zip for parallel lists
for rank, (name, score) in enumerate(zip(names, scores, strict=True), start=1):
    print(rank, name, score)
text
1 Asha 91
2 Ravi 78
3 Meena 85
1 Asha 91
2 Ravi 78
3 Meena 85

Same output, but the second loop cannot index past the end of a list, and strict=True (Python 3.10 and later) turns mismatched lengths into an error instead of silently dropping the extra items (Python docs: zip):

python
try:
    list(zip(names, scores[:2], strict=True))
except ValueError as exc:
    print(exc)
text
zip() argument 2 is shorter than argument 1

Similarly, loop over a dictionary's .items() to get keys and values together, rather than looking each key up again inside the loop.

Let unpacking name the parts

python
record = ("INV-1042", "2026-09-25", 3, 1499.0)
invoice_id, date, qty, price = record            # instead of record[0], record[1]...
first, *middle, last = [10, 20, 30, 40, 50]
a, b = 1, 2
a, b = b, a                                      # swap without a temporary variable
print(invoice_id, qty * price, first, middle, last, a, b)
text
INV-1042 4497.0 10 [20, 30, 40] 50 2 1

Unpacking turns positions into names, which documents the data's shape. If a tuple has more than four or five fields, that is a sign it wants to be a dataclass instead.

Use the dictionary tools that already exist

python
from collections import Counter, defaultdict

orders = [("Hyderabad", "Books"), ("Pune", "Toys"), ("Hyderabad", "Toys"), ("Hyderabad", "Books")]

# Before: manual counting
counts = {}
for city, _ in orders:
    if city not in counts:
        counts[city] = 0
    counts[city] += 1

# After: Counter counts, defaultdict groups
print(Counter(city for city, _ in orders).most_common(1))
by_city = defaultdict(list)
for city, category in orders:
    by_city[city].append(category)
print(dict(by_city))
print({"pin": 500001}.get("phone", "not given"))
text
[('Hyderabad', 3)]
{'Hyderabad': ['Books', 'Toys', 'Books'], 'Pune': ['Toys']}
not given

Counter and defaultdict remove the "is the key there yet?" bookkeeping entirely (Python docs: collections), and dict.get with a default replaces a try/except KeyError for simple lookups. The Python data structures reference covers these containers in depth.

Build strings with join and f-strings

python
parts = ["raw", "clean", "curated"]
path = ""
for p in parts:                     # before: repeated concatenation
    path += p + "/"
print(path.rstrip("/"), "/".join(parts))

total = 4497.0
print(f"Total: {total:>10,.2f} | {qty=}")
text
raw/clean/curated raw/clean/curated
Total:   4,497.00 | qty=3

"/".join(parts) states the intent and avoids building a new string on every loop iteration. Format specifiers inside f-strings handle alignment, thousands separators and decimals; the = specifier prints both the expression and its value, which is handy while debugging. Python f-strings: formatting done right has the full specification.

Check None and emptiness the idiomatic way

python
def summarise(items=None):
    if items is None:          # "was anything passed?" compare to None with is
        items = []
    if not items:              # "is it empty?" rely on truthiness
        return "nothing to report"
    return f"{len(items)} items"

print(summarise(), "|", summarise([]), "|", summarise(["a", "b"]))
text
nothing to report | nothing to report | 2 items

PEP 8 recommends comparing to None with is and using the fact that empty sequences are false, rather than writing len(items) == 0 (PEP 8). Keep the two questions separate, though: if not value is also true for 0 and "", so when zero is a legitimate value, test is None explicitly.

Let with and pathlib manage files

python
from pathlib import Path

report = Path("report.txt")
report.write_text("ok\n", encoding="utf-8")
with report.open(encoding="utf-8") as f:       # closed automatically, even on error
    print(f.readline().strip(), report.suffix, report.stem)
text
ok .txt report

with guarantees the file is closed however the block exits, and pathlib.Path replaces string concatenation of paths with objects that know their name, suffix and parent.

Ask questions of whole collections

any(), all(), sum(), min() and max() accept a generator expression, which replaces a loop with a flag variable:

  • Instead of setting found = False and looping until a condition matches, write any(o.total > 10_000 for o in orders).
  • Instead of counting matches in a loop, write sum(1 for o in orders if o.status == "late").
  • Instead of sorting and taking the first element to find a maximum, write max(orders, key=lambda o: o.total).

Each reads as the question being asked, and any and all stop at the first item that settles the answer.

Attributes, not getters and setters

Code written by people coming from Java often wraps every attribute in get_price() and set_price() methods. In Python, start with a plain attribute. If you later need validation or a computed value, turn it into a property: callers keep writing item.price, and nothing that uses the class has to change. The object-oriented programming guide shows a property protecting a quantity from invalid values.

Functions and naming

  • Names say what things are. snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants, as PEP 8 describes. customers_by_city beats d2.
  • Small functions that do one thing, returning values rather than printing them, so they can be tested and reused.
  • Keyword-only options for flags: export(path, *, overwrite=False) makes calls read overwrite=True instead of a bare True. Python args and kwargs explained explains the syntax.
  • No mutable default arguments. Use None and create the list inside, as summarise does.
  • Errors raised, not hidden. Catch specific exceptions and let the rest surface; see error handling in Python.

Let tools enforce the boring parts

Style arguments are a waste of review time. Three tools settle them automatically:

  1. A formatter rewrites code into one consistent layout. Ruff's formatter and Black are the common choices.
  2. A linter flags likely bugs and unidiomatic patterns: unused imports, shadowed names, mutable defaults. Ruff covers the rules of many older linters in one fast tool (Ruff documentation).
  3. A type checker such as mypy or Pyright reads your type hints and catches whole classes of mistakes before the code runs.

Run them in your editor and in continuous integration, and configure them in pyproject.toml, as described in how to structure a Python project. Then code review can focus on design instead of spacing.

Pythonic does not mean clever

A one-line nested comprehension with two conditions and a walrus operator may be valid Python, but if a colleague needs a minute to read it, it is not Pythonic. PEP 8's own point is that consistency and readability within a project matter more than any individual rule. When an idiom makes the code harder to follow, write the plain version.

Is PEP 8 mandatory?

No. It is the style guide for Python's standard library and a widely shared convention, not a rule the interpreter enforces. Most teams adopt it, usually through a formatter, and document any exceptions.

What is the fastest way to make old code more Pythonic?

Run a linter with the idiom-related rules enabled and a formatter over the codebase, then fix the warnings a module at a time. Tests first, so you can refactor safely.

Do type hints make code more Pythonic?

They make intent explicit, which the Zen of Python favours, and they let tools catch mistakes early. They are optional, so add them where they clarify: public functions, data structures and anything other people call.

Should every loop become a comprehension?

No. Comprehensions are for building a collection from an iterable. Loops with side effects, several steps or error handling are clearer as loops.

Readable code is also testable code; unit testing with pytest is the natural next step. Writing clean Python code, debugging and code optimisation form the testing and best practices module of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course