total=False TypedDict vs Optional Values

TL;DR — total=False on a TypedDict means every key may be absent from the dict. That is a different guarantee from a key that is always present but whose value may be None (str | None). Absence forces you to check with in or read with .get() before use; a nullable value lets you index directly and then narrow away None. Confusing the two is the most common TypedDict mistake.

Presence and nullability are two independent axes, and TypedDict lets you set them separately. total=False moves the presence axis for the whole class: keys become optional. An Optional value type moves the nullability axis for one key: the key is there, but might hold None. This page pulls them apart, because the access patterns and the error codes differ.

Presence versus nullability total controls whether a key is present in the dict, while the value type controls whether a present key may hold None. They are independent. total=False → key may be ABSENT "nickname" not in payload read with .get() or check in payload["nickname"] → [typeddict-item] key might not exist at all value: str | None → key PRESENT "nickname" in payload always index directly, then narrow None payload["nickname"] → str | None value may be null
total=False changes whether the key is there; a nullable value changes what a present key holds.

total=False makes every key optional

Set total=False and none of the keys are guaranteed to exist. A checker then refuses direct subscript access, because the key might be missing:

# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import TypedDict

class UserProfile(TypedDict, total=False):
    display_name: str
    nickname: str
    bio: str

def greet(profile: UserProfile) -> str:
    return "Hi " + profile["nickname"]   # mypy: Key "nickname" of TypedDict "UserProfile"
                                         # may be absent  [typeddict-item]

Every construction below is valid, precisely because absence is allowed — including the empty dict:

# Python 3.11+, mypy 1.10 / pyright 1.1.370
a: UserProfile = {}                                  # OK — all keys optional
b: UserProfile = {"display_name": "Dana"}            # OK
c: UserProfile = {"nickname": "dee", "bio": "hi"}    # OK

Reading an optional key safely

To read an optional key you must prove it is present, or supply a fallback. The two idioms are an in check (which narrows) and .get() (which returns None or a default):

# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
def label(profile: UserProfile) -> str:
    if "nickname" in profile:
        return profile["nickname"]        # narrowed: key is present → str
    return profile.get("display_name", "guest")   # returns str, with default

After if "nickname" in profile: the checker narrows the subscript to the value type and the earlier [typeddict-item] error disappears. .get("display_name") without a default is typed str | None, so you then narrow away the None before using it as a str.

The contrast: a present key with an Optional value

Now the other axis. Keep the dict total=True (the default) so the key is always present, but let its value be null with str | None. The access pattern is the opposite — you index freely, then narrow the value:

# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import TypedDict

class Account(TypedDict):
    user_id: int
    nickname: str | None        # key ALWAYS present; value may be None

def render(acc: Account) -> str:
    name = acc["nickname"]      # OK — key is guaranteed present, type is str | None
    if name is None:
        return f"user-{acc['user_id']}"
    return name                 # narrowed to str

No [typeddict-item] here: acc["nickname"] is always safe because the key exists. What you must handle is the None value, exactly like any other Optional type. If you want both — the key optional and its value nullable — combine them with NotRequired[str | None], which the Required and NotRequired guide covers in depth.

Analyzer behavior

mypy flags direct access to a possibly-absent key with [typeddict-item], the same code it uses for a missing required key or a wrong-typed value. pyright reports possibly-absent access under reportTypedDictNotRequiredAccess, which is more targeted than mypy’s shared code and can be tuned independently. After an in narrowing both checkers agree the key is present. For a nullable value, the diagnostics are the ordinary Optional ones — mypy [union-attr] if you call a method on the un-narrowed value, pyright reportOptionalMemberAccess. See pyright vs mypy for how their optional-access reporting differs in practice.

Runtime vs static analysis total=False exists only for the checker; at runtime the object is a plain dict. Indexing a missing key raises a real KeyError, while a present-but-None value returns None with no error. That is the practical difference the type system is modeling: absence blows up on access, nullability quietly hands you None. Use .get() for the former and an is None check for the latter.

Common mistakes

  • Subscripting a total=False key directly. profile["nickname"] assumes presence; mypy [typeddict-item], pyright reportTypedDictNotRequiredAccess. Guard with in or use .get().
  • Treating total=False as “value can be None.” It does not touch the value type — a present key must still match its declared type. Use str | None (or NotRequired[str | None]) for nullability.
  • Forgetting to narrow a .get() result. profile.get("bio") is str | None; calling .strip() on it directly is mypy [union-attr] / pyright reportOptionalMemberAccess. Narrow the None first.
  • Mixing the two axes by accident. A total=False key with a str | None value can be absent or null; make sure your access handles both, or tighten the schema so only one axis varies.

FAQ

Is total=False the same as making every value Optional? No. total=False says each key may be missing; Optional values say each present key may be None. The first requires presence checks (in/.get()), the second requires None narrowing after a direct index. They can even stack as NotRequired[T | None].

How do I make just one key optional instead of all of them? Leave the class total=True and mark the single key NotRequired[...]. total=False is the whole-class switch; per-key NotRequired is the surgical one, described in the Required and NotRequired guide.

Back to Literal and TypedDict