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
Last updated: 24 September 2026
"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
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)1 Asha 91
2 Ravi 78
3 Meena 85
1 Asha 91
2 Ravi 78
3 Meena 85Same 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):
try:
list(zip(names, scores[:2], strict=True))
except ValueError as exc:
print(exc)zip() argument 2 is shorter than argument 1Similarly, 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
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)INV-1042 4497.0 10 [20, 30, 40] 50 2 1Unpacking 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
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"))[('Hyderabad', 3)]
{'Hyderabad': ['Books', 'Toys', 'Books'], 'Pune': ['Toys']}
not givenCounter 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
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=}")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
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"]))nothing to report | nothing to report | 2 itemsPEP 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
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)ok .txt reportwith 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 = Falseand looping until a condition matches, writeany(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_casefor functions and variables,CapWordsfor classes,UPPER_CASEfor constants, as PEP 8 describes.customers_by_citybeatsd2. - 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 readoverwrite=Trueinstead of a bareTrue. Python args and kwargs explained explains the syntax. - No mutable default arguments. Use
Noneand create the list inside, assummarisedoes. - 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:
- A formatter rewrites code into one consistent layout. Ruff's formatter and Black are the common choices.
- 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).
- 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?
What is the fastest way to make old code more Pythonic?
Do type hints make code more Pythonic?
Should every loop become a comprehension?
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
- PEP 8 – Style Guide for Python Code — Python Enhancement Proposals
- PEP 20 – The Zen of Python — Python Enhancement Proposals
- Built-in functions: zip and enumerate — Python documentation
- collections — Container datatypes — Python documentation
- Ruff documentation — Astral
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.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.