Writing Pythonic Code
Python has a distinctive philosophy captured in the Zen of Python (import this). Writing "Pythonic" code means embracing the idioms and patterns that the language was designed around, rather than translating habits from other languages. This guide covers the most important idioms every Python developer should know.
EAFP vs LBYL
Python favors EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap). Instead of checking whether an operation will succeed before attempting it, try the operation and handle the exception if it fails.
# LBYL (non-Pythonic)
if "key" in my_dict:
value = my_dict["key"]
else:
value = "default"
# EAFP (Pythonic)
try:
value = my_dict["key"]
except KeyError:
value = "default"
# Even more Pythonic — use .get()
value = my_dict.get("key", "default")
EAFP is preferred because in Python, exceptions are cheap and common. It also avoids race conditions where the state might change between the check and the operation.
Click "Run" to execute your codeList Comprehensions vs For Loops
List comprehensions are one of Python's most beloved features. They express the creation of a new list from an existing iterable in a single, readable expression.
# Traditional loop
squares = []
for x in range(10):
squares.append(x ** 2)
# List comprehension (Pythonic)
squares = [x ** 2 for x in range(10)]
# With filtering
evens = [x for x in range(20) if x % 2 == 0]
# With transformation
names = ["alice", "bob", "charlie"]
capitalized = [name.title() for name in names]
Use comprehensions when they are clear and readable. If the logic gets complex, a regular loop is better.
Click "Run" to execute your codeTuple Unpacking and Starred Assignments
Python lets you unpack sequences into variables in a single statement. This makes code cleaner and more expressive.
# Basic unpacking
x, y, z = (1, 2, 3)
first, second = "AB"
# Swap without temp variable
a, b = b, a
# Starred assignment — capture the rest
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5
# Unpack in loops
pairs = [(1, "a"), (2, "b"), (3, "c")]
for number, letter in pairs:
print(f"{number}: {letter}")
Click "Run" to execute your codeContext Managers (the with Statement)
Context managers ensure resources are properly cleaned up, even if an exception occurs. Always use with when working with files, locks, database connections, or any resource that needs cleanup.
# Without context manager (risky)
f = open("file.txt")
data = f.read()
f.close() # What if read() raised an exception?
# With context manager (Pythonic)
with open("file.txt") as f:
data = f.read()
# File is automatically closed, even if an error occurs
You can also create your own context managers:
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.time()
yield
elapsed = time.time() - start
print(f"{label}: {elapsed:.3f}s")
with timer("Processing"):
# do work here
total = sum(range(1_000_000))
Click "Run" to execute your codeF-Strings for Formatting
F-strings (formatted string literals, introduced in Python 3.6) are the most readable and fastest way to format strings.
name = "Alice"
age = 30
# F-string (recommended)
print(f"My name is {name} and I'm {age} years old.")
# Expressions inside braces
print(f"Next year: {age + 1}")
print(f"Name uppercased: {name.upper()}")
# Format specifiers
pi = 3.14159
print(f"Pi: {pi:.2f}") # 3.14
print(f"Big number: {1_000_000:,}") # 1,000,000
print(f"Padded: {42:05d}") # 00042
print(f"Percentage: {0.856:.1%}") # 85.6%
Click "Run" to execute your codeenumerate() and zip()
Never use range(len(...)) to iterate with indices. Use enumerate() instead. And when iterating over multiple sequences in parallel, use zip().
# Bad
for i in range(len(colors)):
print(f"{i}: {colors[i]}")
# Good
for i, color in enumerate(colors):
print(f"{i}: {color}")
# With custom start index
for i, color in enumerate(colors, start=1):
print(f"{i}: {color}")
# zip() for parallel iteration
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
Click "Run" to execute your codeTruthiness and Falsy Values
Python has a clear notion of which values are "truthy" and which are "falsy." Use this instead of explicit comparisons.
The falsy values in Python are: None, False, 0, 0.0, "" (empty string), [] (empty list), () (empty tuple), {} (empty dict), set(), and any object whose __bool__() returns False or __len__() returns 0.
Everything else is truthy.
# Non-Pythonic
if len(my_list) > 0:
process(my_list)
if name != "":
greet(name)
if x is not None and x != 0:
use(x)
# Pythonic
if my_list:
process(my_list)
if name:
greet(name)
if x:
use(x)
Click "Run" to execute your codeThe any() and all() Builtins
any() returns True if at least one element is truthy. all() returns True if every element is truthy. They short-circuit for efficiency.
numbers = [2, 4, 6, 8, 10]
# Check if any number is odd
has_odd = any(n % 2 != 0 for n in numbers)
# Check if all numbers are positive
all_positive = all(n > 0 for n in numbers)
Click "Run" to execute your codeDictionary Techniques
Python dictionaries are powerful and have many idiomatic patterns.
.get() for Safe Access
# Raises KeyError if missing
value = my_dict["missing_key"]
# Returns default instead
value = my_dict.get("missing_key", "default")
defaultdict for Automatic Defaults
from collections import defaultdict
# Group items by category
word_lengths = defaultdict(list)
for word in ["hi", "hey", "hello", "go", "goodbye"]:
word_lengths[len(word)].append(word)
Dict Comprehensions
# Invert a dictionary
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
# Filter a dictionary
scores = {"Alice": 95, "Bob": 72, "Charlie": 88, "Diana": 60}
passed = {name: score for name, score in scores.items() if score >= 75}
Merging Dictionaries
# Python 3.9+ merge operator
merged = dict1 | dict2
# Python 3.5+ unpacking
merged = {**dict1, **dict2}
Click "Run" to execute your codePythonic OOP: Duck Typing and Polymorphism
Python's approach to object-oriented programming differs fundamentally from languages like Java or C++. Understanding these differences is key to writing idiomatic Python.
Duck Typing: "If It Quacks Like a Duck"
Python uses duck typing rather than strict type hierarchies. If an object has the methods and attributes you need, you can use it regardless of its actual class. This is Python's core approach to polymorphism.
# You don't need a shared base class!
class Duck:
def speak(self):
return "Quack!"
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
# This function works with ANY object that has a speak() method
def animal_sound(animal):
return animal.speak()
This is fundamentally different from languages that require a shared interface or base class. In Python, structural compatibility is what matters.
Click "Run" to execute your codeComposition Over Inheritance
Python strongly favors composition over inheritance. Instead of building deep class hierarchies, compose objects from smaller, focused components.
# Inheritance approach (fragile, tightly coupled)
class Animal:
def __init__(self, name):
self.name = name
class FlyingAnimal(Animal):
def fly(self):
return f"{self.name} is flying"
class SwimmingAnimal(Animal):
def swim(self):
return f"{self.name} is swimming"
# What about a duck that can both fly AND swim?
# Multiple inheritance gets messy fast.
# Composition approach (flexible, decoupled)
class FlyAbility:
def fly(self):
return "flying"
class SwimAbility:
def swim(self):
return "swimming"
class Duck:
def __init__(self, name):
self.name = name
self._flyer = FlyAbility()
self._swimmer = SwimAbility()
def fly(self):
return f"{self.name} is {self._flyer.fly()}"
def swim(self):
return f"{self.name} is {self._swimmer.swim()}"
When to use inheritance:
- When there is a genuine "is-a" relationship
- When you need to work with existing frameworks that expect it (e.g., Django models)
- For mixins that add small, orthogonal behaviors
When to use composition:
- When you want to combine behaviors from multiple sources
- When the relationship is "has-a" rather than "is-a"
- When you want to swap implementations at runtime
- When you want loose coupling between components
Click "Run" to execute your codeWhen Multiple Inheritance Makes Sense: Mixins
Multiple inheritance in Python is best used for mixins -- small, focused classes that add a single piece of functionality. A mixin should not be instantiated on its own and should not have its own __init__.
class JsonMixin:
"""Adds JSON serialization to any class with a __dict__."""
def to_json(self):
import json
return json.dumps(self.__dict__)
class LogMixin:
"""Adds simple logging capability."""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class User(JsonMixin, LogMixin):
def __init__(self, name, email):
self.name = name
self.email = email
Click "Run" to execute your codeThe Diamond Problem and MRO
When a class inherits from two classes that share a common ancestor, Python needs a way to determine which method to call. This is the Diamond Problem, and Python solves it with the Method Resolution Order (MRO), using the C3 linearization algorithm.
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return "Hello from B"
class C(A):
def greet(self):
return "Hello from C"
class D(B, C):
pass
# Which greet() does D use? Check the MRO:
# D -> B -> C -> A
print(D.__mro__)
The MRO follows these rules:
- A subclass comes before its parents
- Parents are checked in the order they are listed
- A class only appears once in the MRO
Click "Run" to execute your codeAbstract Base Classes (ABCs) for Interfaces
When you do want to enforce that subclasses implement certain methods, use Abstract Base Classes from the abc module. ABCs are Python's way of defining interfaces.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
# This would raise TypeError if you try to instantiate:
# shape = Shape() # TypeError!
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
ABCs enforce the contract at instantiation time, not at definition time. If you forget to implement an abstract method, you get a clear error when you try to create an instance.
Click "Run" to execute your codePros and Cons Summary
| Approach | Pros | Cons |
|---|---|---|
| Inheritance | Code reuse, polymorphism through class hierarchy, works well with frameworks | Tight coupling, fragile base class problem, deep hierarchies are hard to understand |
| Composition | Loose coupling, flexible, easy to test and swap parts | More boilerplate, less automatic code reuse |
| Mixins | Reusable behaviors, no deep hierarchies | MRO can be confusing, order of bases matters |
| Duck typing | Maximum flexibility, no coupling at all | No compile-time checking, errors only at runtime |
| ABCs | Enforced contracts, clear interfaces | Extra boilerplate, can feel un-Pythonic if overused |
The Pythonic approach is generally: prefer duck typing and composition, use inheritance sparingly, and reach for ABCs only when you need enforced contracts.
What's Next?
Now that you know how to write idiomatic Python, check out Common Python Pitfalls to learn about the mistakes that trip up even experienced developers.