I’m working with a codebase with a lot of structured data being passed around, and I’m looking for a type-safe way to pass data to functions without having to import a bunch of types at every call site. I’m currently using TypedDicts, since it’s possible to call functions expecting TypedDicts with dictionary literals, but as soon as you store a literal-constructed dictionary in a variable, the type checker complains.
For example:
from typing import TypedDict
class CupcakeOrder(TypedDict):
flavor: str
spice_level: int
class RequestForCupcake(TypedDict):
flavor: str
spice_level: int
def bake(co: CupcakeOrder) -> None: ...
# OK
co = CupcakeOrder(flavor="Berry", spice_level=0)
bake(co)
# OK
rfc = RequestForCupcake(flavor="Berry", spice_level=0)
bake(rfc)
# OK
bake({"flavor": "Berry", "spice_level": 0})
# Causes a type error
untyped_dict = {"flavor": "Berry", "spice_level": 0}
bake(untyped_dict)
Is there any way to extend the type checker’s data shape tracking when variables get involved, either for TypedDicts or NamedTuples or some other structural-record-like category of types? e.g., something like:
I don’t see a way to avoid importing the type symbol unless you want to re-declare the full type in each file where it’s used.
I’m curious why you’re reluctant to import the types into your code. A smart editor like VS Code or PyCharm will automatically insert the import statements for you, so it’s pretty painless — at least I find it to be so.
Thanks for the response. And yeah the explicit need to import structural types is what I’m trying to avoid.
As for why, I’d say part of it comes down to the same reasons that people generally don’t go out of their way to add superflouous type annotations to variables storing objects in Typescript, or (as far as I know) records in OCaml. For example, I often have a dozen or so short scripts whose only purpose is to construct a configuration object for some simulation or analysis pipeline, and then run it. For these, using dictionaries or from types import SimpleNamespace as Ns is more convenient and (arguably) more readable, but then you lose type checking.
Another reason is that I’m looking into the feasibility of procedurally generating typed Python bindings for a shared library based on a C header that I’d parse using the cffi library. And in this case, some of the generated types wouldn’t have the most straightforward names.
import ctypes, typing
import numpy as np
class SimulationSpec(ctypes.Structure):
# How the attributes are stored
_fields_ = [
("duration", ctypes.c_float),
("timestep_size", ctypes.c_float),
("squirrel_density", ctypes.c_float),
("output_buffer", ctypes.POINTER(ctypes.c_int)),
]
# What you actually get back from attribute reads
duration: float
timestep_size: float
squirrel_density: float
output_buffer: "ctypes._Pointer[ctypes.c_int]"
class _ConvertibleToSimulationSpec(typing.TypedDict):
duration: float
timestep_size: float
squirrel_density: float
output_buffer: np.ndarray
def run_simulation(arg0: _ConvertibleToSimulationSpec) -> None:
# Convert `arg0` to a `SimulationSpec` and then call the C implementation.
...