---
title: "When to Use Dataclasses, Generators, and Try/Except in Python"
description: "Phase 2 of the mental checklist for Python developers. Navigating dictionaries vs dataclasses, eager evaluation vs lazy execution, and error handling philosophies."
pubDate: "2023-05-25"
lang: "en"
slug: "when-to-use-dataclasses-generators-in-python"
url: "https://hoangyell.com/when-to-use-dataclasses-generators-in-python/"
author: "HoangYell"
translation_vi: "https://hoangyell.com/vi/when-to-use-dataclasses-generators-in-python/"
categories: ["Software Craft"]
tags: ["python", "software-design", "best-practices"]
heroImage: "https://raw.githubusercontent.com/yellcamap/img/refs/heads/main/photo-1515879218367-8466d910aaa4.webp"
---

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
```python
# 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
```python
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
```python
# 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
```python
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
```python
# 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
```python
# 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
```python
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 `cls` → **Put it outside the class as Python function!**

**Explicit vs `**kwargs`:**
* Business logic & APIs → **Explicit parameters**
* Decorators & wrappers → **`*args, **kwargs`**
