Featured image of post When to Use Dataclasses, Generators, and Try/Except in Python

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 dict or a dataclass?
  • Do I use a list comprehension here, or a generator?
  • Should I check if the key exists with if, or just jump in with try/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

1
2
3
# 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

1
2
3
4
5
6
7
8
9
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 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 if statements.

Example: Avoiding race conditions with EAFP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 self or cls. 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

1
2
3
4
5
6
7
8
9
# 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

1
2
3
4
5
6
7
8
# 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

1
2
3
4
5
6
7
8
9
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 self or clsPut it outside the class as Python function!

Explicit vs **kwargs:

  • Business logic & APIs → Explicit parameters
  • Decorators & wrappers → *args, **kwargs
Made with laziness love 🦥

Subscribe to My Newsletter