---
title: "Pydantic Model Passed Where Dict Expected — Silent .get() Failure"
aliases: [pydantic-dict-interface, pydantic-get-none, pydantic-vs-dict]
tags: [pydantic, python, fastapi, celery, debugging, type-safety, silent-failure]
sources:
  - "daily/2026-04-28.md"
created: 2026-04-28
updated: 2026-04-28
---

# Pydantic Model Passed Where Dict Expected — Silent .get() Failure

When a function written to accept a `dict` is called with a Pydantic model, calling `.get("key")` on the model returns `None` (since `.get()` is not a standard Pydantic method) instead of raising an `AttributeError` or `TypeError`. The function continues executing with `None` values — producing silent wrong behavior, hangs, or downstream failures rather than a clear error.

## Key Points

- **`.get()` on a Pydantic model returns `None`** — Pydantic v1 models don't have a `.get()` method; calling it doesn't raise `AttributeError`, it silently falls back to Python's default `None` (or Pydantic's own method resolution)
- **Symptom:** Task or function appears to run but produces wrong output, hangs, or sends empty/null data to downstream systems — no exception raised at the call site
- **Detection:** Add `assert isinstance(data, dict)` or type-hint the parameter as `dict` and run mypy; or check the call site to see what type is actually being passed
- **Fix option A:** Convert at the call site — `task.delay(banner_copy.model_dump())`
- **Fix option B:** Update the function to accept Pydantic model — `def refine_variant_copy(copy: BannerCopy)` and use `.field` attribute access

## Details

### How the Bug Manifests

In the barclays-banner-builder, a Celery task `refine_variant_copy` was written expecting a `dict`:

```python
# tasks.py — written expecting dict
def refine_variant_copy(copy: dict, ...):
    headline = copy.get("headline")   # ← works if copy is dict
    body_text = copy.get("body_text") # ← returns None if copy is Pydantic model
    # Function continues with headline=None, body_text=None
    # AI call gets empty inputs → returns garbage or hangs waiting
```

The call site passed a `BannerCopy` Pydantic model:

```python
# caller
banner_copy = BannerCopy(headline="Buy Now", body_text="Great offer")
refine_variant_copy.delay(banner_copy, ...)  # ← wrong type passed
```

Pydantic v1 models do implement `__getitem__` (for subscript access like `copy["key"]`), but not the dict `.get()` method with its default-return semantics. When Python resolves `copy.get("headline")` on a Pydantic model, it finds no such method and returns `None` rather than raising — this is because `BaseModel` does not define `.get()`, so Python's attribute lookup falls back to `None` in some configurations, or Pydantic provides a stub.

The result: every `.get()` call returns `None`. The function keeps running with all-`None` values, silently producing wrong output.

### Detection

**Runtime check at function entry:**
```python
def refine_variant_copy(copy, ...):
    if not isinstance(copy, dict):
        raise TypeError(f"Expected dict, got {type(copy).__name__}")
    ...
```

**Static analysis (mypy):**
```python
def refine_variant_copy(copy: dict, ...) -> str:
    ...
# mypy will flag: Argument 1 has incompatible type "BannerCopy"; expected "Dict[str, Any]"
```

**Grep for the pattern:**
```bash
grep -n "\.get(" tasks.py
# Then check each call site to see what type is passed
```

### Fix

**Option A — Convert at call site (minimal change):**
```python
# Convert Pydantic model to dict before passing to task
refine_variant_copy.delay(banner_copy.model_dump(), prompt, job_id)
# Pydantic v1: banner_copy.dict()
# Pydantic v2: banner_copy.model_dump()
```

**Option B — Update function signature to accept Pydantic model (cleaner):**
```python
from app.models import BannerCopy

def refine_variant_copy(copy: BannerCopy, ...) -> str:
    headline = copy.headline        # attribute access, not .get()
    body_text = copy.body_text
    ...
```

Option B is preferable when the function is an internal API — it makes the type contract explicit and mypy can catch misuse. Option A is safer when the function is a Celery task that may receive serialized data from a queue (where dicts are the natural representation after JSON deserialization).

### General Rule

Before writing a function that calls `.get()` on its arguments, decide explicitly: does this function take a `dict` or a typed model? Document it with a type annotation. If the codebase mixes both, add `isinstance` guards or use `.model_dump()` at the boundary where Pydantic models enter dict-oriented code.

## Related Concepts

- [[wiki/concepts/zustand-async-hydration]] — another silent failure pattern from the same barclays-banner-builder session
- [[wiki/concepts/pydantic-v2-alias-id-gotcha]] — other Pydantic serialization pitfalls

## Sources

- [[daily/2026-04-28.md]] — barclays-banner-builder: `refine_variant_copy` task accepted dict but was called with BannerCopy Pydantic object; `.get()` returned None silently; caused hang in AI refinement pipeline
