---
title: "Stop Passing Raw Dicts Around in Python: Safer Models for 2026"
description: "A practical 2026 guide to keeping Python dictionaries under control by converting raw JSON into dataclasses, Pydantic models, TypedDict contracts, and clear mapping types."
author: Aleksei Aleinikov
date: 2026-03-17
updated: 2026-06-28
lang: en
tags: [python, pydantic, data-models, type-safety, validation, clean-code]
canonical: https://www.alekseialeinikov.com/en/blog/topics/programming/stop-passing-raw-dicts-around-python-models-2026
source: alekseialeinikov.com
---

# Stop Passing Raw Dicts Around in Python: Safer Models for 2026

Python dictionaries are convenient until they quietly become the shape of your whole application.

It usually starts with a small script. A few keys, one JSON response, one quick transformation. Everything is readable. Then the script becomes a service, the service becomes a product, and suddenly the same dictionary shape is being created, mutated, and guessed across dozens of files.

![It started as a simple script, and now the dict has a life of its own.](https://www.alekseialeinikov.com/blog/1_KtSgqz0LdjgRisqN8r-Skw.webp)

The problem is not that dictionaries are bad. The problem is using them as invisible domain models.

## Where Dicts Start Hurting

**A dictionary can store almost anything under almost any key.** That flexibility is useful for transport data, but dangerous for core application logic.

If one function expects `firstName`, another expects `first_name`, and a third quietly removes `permissions`, there is no single obvious place to understand the final shape.

The data model becomes tribal knowledge. IDE autocomplete gets weaker. Refactoring gets slower. Bugs appear as `KeyError`, wrong defaults, or missing fields only after the code path runs in production.

## Treat Dicts as Transport, Not Domain Objects

Raw dictionaries are perfect at system boundaries:

- incoming JSON from an API,
- decoded message queue payloads,
- loosely structured config,
- temporary key-value mappings.

But once the data enters your application, convert it into something with a name, fields, and behavior.

Imagine a user API returns this payload:

```json
{
  "user_id": 42,
  "profile": {
    "firstName": "Alice",
    "lastName": "Wonderland"
  },
  "permissions": ["read", "write", "admin"]
}
```

Passing that raw dictionary everywhere is tempting. It is also how every future field rename becomes painful.

## Option 1: A Small Domain Class

**The simplest fix is a class that represents the concept your application actually uses.**

```python
import requests


class User:
    def __init__(
        self,
        user_id: int,
        first_name: str,
        last_name: str,
        permissions: list[str],
    ):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name
        self.permissions = permissions

    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"


def fetch_user(user_id: int) -> User:
    response = requests.get(f"https://example.com/api/users/{user_id}", timeout=10)
    response.raise_for_status()
    data = response.json()

    return User(
        user_id=data["user_id"],
        first_name=data["profile"]["firstName"],
        last_name=data["profile"]["lastName"],
        permissions=data["permissions"],
    )
```

The code is a little longer, but now the conversion is centralized. If the external API changes, you update `fetch_user` instead of hunting through the whole codebase.

## Option 2: Dataclasses

For plain data objects, `dataclasses` remove much of the boilerplate.

```python
from dataclasses import dataclass


@dataclass(frozen=True)
class User:
    user_id: int
    first_name: str
    last_name: str
    permissions: list[str]

    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"
```

`frozen=True` makes the object immutable. That is useful when you want to prevent accidental mutation after parsing.

If you need a changed version, create a new instance or use `dataclasses.replace`.

## Option 3: Pydantic

**When you need validation, type conversion, aliases, nested models, or serialization, Pydantic is often the strongest choice.**

```python
from pydantic import BaseModel, ConfigDict, Field


class Profile(BaseModel):
    first_name: str = Field(alias="firstName")
    last_name: str = Field(alias="lastName")


class User(BaseModel):
    model_config = ConfigDict(frozen=True)

    user_id: int
    profile: Profile
    permissions: list[str]

    def full_name(self) -> str:
        return f"{self.profile.first_name} {self.profile.last_name}"
```

Now invalid input fails at the boundary. The **rest** of the application receives a predictable object instead of a mystery dictionary.

## Option 4: TypedDict for Gradual Refactoring

**Sometimes you cannot replace dictionaries immediately.** A legacy project may already pass dicts through too many layers.

`TypedDict` gives you a migration step. At runtime it is still a normal dictionary, but static checkers can catch missing keys and wrong value types.

```python
from typing import TypedDict


class UserDict(TypedDict):
    user_id: int
    first_name: str
    last_name: str
    permissions: list[str]


user_data: UserDict = {
    "user_id": 42,
    "first_name": "Alice",
    "last_name": "Wonderland",
    "permissions": ["read", "write"],
}
```

It is not as strong as a real model, but it is much better than an untyped `dict[str, object]`.

## When Dictionaries Are Still the Right Tool

Dictionaries are still excellent for genuine mappings:

```python
api_keys: dict[str, str] = {
    "service_a": "12345ABC",
    "service_b": "ABCDE12345",
}
```

If the structure really is a key-value store, keep it as a mapping. You can also annotate APIs with `Mapping[str, str]` or `MutableMapping[str, str]` to show whether callers are allowed to mutate it.

The key question is simple: is this a lookup table, or is this pretending to be an entity?

## Practical Rule for 2026

**Use dictionaries at the edges.**

Convert external payloads into named models as soon as possible.

Use dataclasses for lightweight internal structures, Pydantic for validation-heavy data, and TypedDict when you need a gradual path out of dictionary-heavy legacy code.

The earlier you make data shapes explicit, the easier your **Python** project will be to refactor, test, and maintain.
