Python's not really built for that AFAIK, though. In languages built for it, you can type your dicts/hashes/maps/whatever and its easier to see what they are/know where the functions that operate on them live. I'm most familiar with Elixir which has structs which are simply specialized map (analogous to dict in Python) where their "type" is the name of the module they belong to. There can only be one struct per module, In this sense is easy to know exactly where its functions live and is almost like a class with the very key difference that modules are not stateful.
> In languages built for it, you can type your dicts/hashes/maps/whatever and its easier to see what they are/know where the functions that operate on them live.
I think I must be misunderstanding what you mean by that, because I can very much do that in Python.
That's what I thought. I obviously don't know Python well enough and didn't know you can name dicts (like, beyond setting them to a variable). I guess you can export from a module so they are prefixed! Didn't think of that one earlier.
I'm not sure what you mean by naming dicts, but Python has TypedDict, where you can define the names and types of specific keys. They only exist for type checking and behave exactly as a normal dict at runtime.
In modern typed Python, you can instead use dataclasses, NamedTuples (both in the standard library), attrs or Pydantic (both third-party) to represent structs/records, the latter also providing validation. Still, TypedDicts are helpful when interfacing with older code that uses dicts for heterogeneous data.
My main gripe with them is that different TypedDicts are not compatible with each other. For example, it would be very helpful if a dict with x:str and y:str fields were considered superclasses of dicts with x:str, y:str and z:str like they are in TypeScript, but they aren't. They are considered different types, limiting their usability in some contexts.
When using homogenous dicts, you can still use dict[str, T], and T can be Any if you don't want to type the whole thing. You can use any hashable type instead of str for keys. I often do that when reading JSON from dynamically typed dict[str, Any] to dataclasses.
That needs to be explicit for any interacting types. You must define separate classes and explicitly define their hierarchy. This is fine if you control all the types, but it breaks down quickly. The best example is having two TypedDicts with the same members; in Python, you cannot use one instead of the other.
from typing import TypedDict
class A(TypedDict):
a: int
b: str
class B(TypedDict):
a: int
b: str
def f(a: A) -> None: pass
b = B(a=1, b='b')
f(B) # mypy error: Argument 1 to "f" has incompatible type "type[B]"; expected "A" [arg-type]
On the other hand, this is legal in Typescript:
interface A {
a: number;
b: string;
}
interface B {
a: number;
b: string;
}
function f(a: A) {}
const b: B = {a: 1, b: 'b'};
f(b);
This is most useful when A has a subset of B's attributes, like this (which also doesn't work in Python):
interface A {
a: number;
}
interface B {
a: number;
b: string;
}
function f(a: A) {}
const b: B = {a: 1, b: 'b'};
f(b);
Python classes are basically dictionaries that have a distinct type bound to them. Alternatively you can subclass from dictionary to give yourself a distinct type but still be a dictionary. Slotted classes are basically named tuples (and of course, Python has actual named tuples and dataclasses), so there's a lot of ways to "tag" a collection with a specific type in mind.