When to Use Dataclasses, Generators, and Try/Except in Python
Phase 2 of the mental checklist for Python developers. Navigating dictionaries vs dataclasses, eager evaluation vs lazy execution, and error handling philosophies.
You’ve mastered the basics of classes and functions, but Python usually offers multiple ways to solve the same problem. Momentum slows down when you start overthinking:
- Should I pass this data around as a
dictor adataclass? - Do I use a list comprehension here, or a generator?
- Should I check if the key exists with
if, or just jump in withtry/except? - Should this be a
@staticmethod, or just a standalone function outside the class? - Should I write out all the parameters clearly, or just accept
**kwargs?
This post is Phase 2 of the repeatable mental checklist. Here are 5 more everyday Python design decisions with code examples.
1) Dictionary vs dataclass
Use a dict when:
- The keys are determined dynamically at runtime (e.g., aggregating data by dynamic user IDs).
- The data is a simple, unstructured payload passing through your system.
- You need to serialize it down to JSON immediately.
Use a dataclass when:
- You know the exact fields ahead of time (fixed schema).
- You want typo-protection (IDE autocomplete and type checkers like
mypy). - You need to attach behavior (methods) to the data later.
Example: The typo trap with dicts
# With dicts, typos are runtime errors (or silent bugs)
user = {"first_name": "Alice", "role": "admin"}
print(user.get("firstname")) # Returns None. Silent bug!
Example: Dataclasses give you safety
from dataclasses import dataclass
@dataclass
class User:
first_name: str
role: str
user = User(first_name="Alice", role="admin")
print(user.first_name) # IDE autocompletes this. Typo? mypy catches it before you run.
2) List Comprehension (Eager) vs Generator (Lazy)
Use a List Comprehension [...] when:
- You need to know the length of the results (
len()). - You need to iterate over the collection multiple times.
- The dataset is small enough to fit comfortably in memory.
- You need to sort or slice the data from the end.
Use a Generator Expression (...) or yield when:
- You are dealing with a massive dataset (logs, large files, database cursors).
- You are chaining multiple processing steps together (pipelines).
- You might break out of the loop early (e.g., finding the first match).
Example: Don’t load the whole file into memory
# BAD: Reads the entire 5GB log file into memory as a list
def get_error_logs(file_path: str) -> list[str]:
with open(file_path) as f:
return [line for line in f if "ERROR" in line]
# GOOD: Yields one line at a time (Lazy evaluation)
def stream_error_logs(file_path: str):
with open(file_path) as f:
for line in f:
if "ERROR" in line:
yield line
# Finding the FIRST error is instant and takes 0 memory overhead
first_error = next(stream_error_logs("app.log"))
3) try/except (EAFP) vs if/else (LBYL)
Python embraces EAFP: “It’s Easier to Ask for Forgiveness than Permission.” Many other languages prefer LBYL: “Look Before You Leap.”
Use if/else (LBYL) when:
- The failure case is very common (e.g., 30% of the time). Exceptions are slow if they are raised constantly.
- Checking the condition is cheap and robust.
Use try/except (EAFP) when:
- The “happy path” happens 99% of the time.
- You want to avoid race conditions (e.g., a file is deleted after you check if it exists, but before you open it).
- The code is cleaner without deeply nested
ifstatements.
Example: Avoiding race conditions with EAFP
import os
# LBYL (Look before you leap) - Race condition possible!
if os.path.exists("config.json"):
# What if another process deletes the file right HERE? Crash!
with open("config.json") as f:
config = f.read()
# EAFP (Easier to ask for forgiveness) - Pythonic!
try:
with open("config.json") as f:
config = f.read()
except FileNotFoundError:
config = "{}"
4) @staticmethod vs Module-Level Function
Use a module-level function when:
- The function doesn’t need
selforcls. In Python, you don’t need to force everything into a class like you do in Java or C#.
Use @staticmethod when (Rarely):
- The function strictly belongs to the class namespace logically, and moving it outside would confuse the API.
- You are grouping it tightly with other specific class methods.
Example: Just use a function
# Java-brain in Python
class MathUtils:
@staticmethod
def calculate_tax(amount: float) -> float:
return amount * 0.2
# Pythonic way
def calculate_tax(amount: float) -> float:
return amount * 0.2
5) Explicit Parameters vs *args, **kwargs
Use explicit parameters when:
- You are building a public API, service class, or business logic core.
- Discoverability matters (other developers need to know exactly what to pass).
- You want type-checking and IDE support.
Use *args, **kwargs when:
- You are writing a wrapper, decorator, or middleware that passes arguments blindly to another function.
- You are subclassing and passing arguments up to
super().__init__().
Example: The frustration of hidden kwargs
# Frustrating API
def create_customer(**kwargs):
# What does this take? email? name? first_name? Who knows!
db.save(kwargs)
# Excellent API
def create_customer(email: str, name: str, phone: str | None = None):
db.save({"email": email, "name": name, "phone": phone})
Example: The perfect use case for kwargs
import time
def time_it(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # Blind pass-through. Perfect.
print(f"Took {time.time() - start}s")
return result
return wrapper
Compact decision cheat-sheet (Phase 2)
Dict vs Dataclass:
- Runtime keys & unstructured → Dict
- Fixed schema & IDE safety → Dataclass
List vs Generator:
- Need length & multiple passes → List
- Massive data & step-by-step pipelining → Generator
If/else vs Try/except:
- Common failure or cheap check → If/else
- Happy path 99% of the time or preventing race conditions → Try/except
Static method vs Function:
- Doesn’t need
selforcls→ Put it outside the class as Python function!
Explicit vs **kwargs:
- Business logic & APIs → Explicit parameters
- Decorators & wrappers →
*args, **kwargs
Related posts
-
When to Use Classes vs. Functions in Python: A Design Checklist
A repeatable mental checklist for Python developers to decide when to use classes, functions, instance state, and dependency injection.
-
MoneyPrinterV2: What 18,000 Stars Worth of Automated Content Actually Looks Like
An assembly line for AI content — local LLMs write the script, KittenTTS reads it, Gemini paints the pictures. The video uploads itself.
-
Unleashing the Super Agent Harness: A Deep Dive into Bytedance's DeerFlow
Discover how DeerFlow 2.0 transforms from a deep research tool into a full-fledged agent harness with sandboxing, sub-agents, and persistent memory.
-
OpenBB Explained: The Open Data Platform for Investment Research
A deep dive into OpenBB, the open-source platform that unifies financial data APIs into a single interface for Python developers, analysts, and AI agents.