Skip to content

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.

5 min read Tiếng Việt
When to Use Dataclasses, Generators, and Try/Except in Python

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

# 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 if statements.

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 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

# 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 self or clsPut it outside the class as Python function!

Explicit vs **kwargs:

  • Business logic & APIs → Explicit parameters
  • Decorators & wrappers → *args, **kwargs

Related posts

You found a tiny easter egg. Keep poking around!