Allow local class/type definitions inside TypedDict

I wonder, whether TypedDict-definitions like the following could be “legalized”:

class HoverParams(TypedDict):
    class Position_0(TypedDict):
        line: int
        character: int
    textDocument: str
    position: Position_0

Definitions like this make sense, in particular, when translating TypeScript class or interface definitions to Python, where it is a common pattern use local anonymous interfaces as types. The TypeScript-source for the above example is:

interface HoverParams {
	textDocument: string; /** The text document's URI in string form */
	position: { line: uinteger; character: uinteger; };
}

The Python interpreter accepts the Python-code above without problems. Hover, TypeCheckers like Pylance complain about this pattern, because PEP 589 (TypedDict) demands that

The class body should only contain lines with item definitions of the form key: value_type, optionally preceded by a docstring. The syntax for item definitions is identical to attribute annotations, but there must be no initializer, and the key name actually refers to the string value of the key instead of an attribute name.

In my opinion this rule should be relaxed to somtheing like: “The class body should only contain lines with item definitions … or other, local, TypedDict-definitions”

Of course, there are other possible workarounds as suggested, here: Mypy/Pylance do not accept non-type annotations inside TypedDict · Issue #9 · jecki/ts2python · GitHub However, this seems less elegant and then, since it works in practice, I do not see a reason why the definition of local TypedDict classes inside other TypedDict classes should not be allowed - or am I missing something?

1 Like

I think the only way this makes any sense, is if it were legal to say HoverParams.Position_0 [1] to refer to the inner type, but even then I don’t think this is very useful.

Generally it’s nicer to provide all your types publically, I don’t think the concept of an anonymous type makes sense in Python, you are just forcing someone else to redefine your anonymous type for you later on. Anonymous classes are actually a common headache when writing stubs, where you end up having to copy the anonymous class to the global scope with a @type_check_only or create a Protocol which matches the anonymous class.


  1. Why the numbered suffix btw? TypedDict is a structural type, so if you have multiple positions with the same structure they should all share the same TypedDict, creating multiple TypedDict with the same structure is semantically the same as creating aliases ↩︎

I mean, it wouldn’t be an anonymous type at all, it has an obvious name.

I’m really on the fence here.

This definitely has some utility - defining inner classes is not uncommon and not just in Python, and given that the original definition is valid Python already, the barrier to doing this is definitely going to be lower.

But on the other hand, the engineering task of moving the inner class definition up a couple of lines is tiny.

If it were up to me, I’d encourage someone who wanted to try to put this feature in, but I personally wouldn’t spend my time on it unless I wanted to find out more about how Python type checking internals worked anyway.

There is work on a PEP that would allow specifying inline typeddict definitions. Here is a thread about it. Pyright has experimental support for it.

Your example would be written as

class HoverParams(TypedDict):
    textDocument: str
    position: TypedDict[{"line": int, "character": int}]

Thank you very much for telling me about this discussion! I am happy to hear that inlined typed dict definitions are on the way. This is more than I hoped for. This way nested type dict definition will at least approach the readbility of their TypeScript-counterparts. I like the suggested syntax “TypedDict[{…}]”.
I hope this soon finds its way into “typing_extensions”, so it can be used in practice.

It is true, “the engineering task of moving the inner class definition up a couple of lines is tiny”. It’s more about readbility. Here is another plausible example, why this is so: Mailman 3 [Typing-sig] Re: Inline syntax for TypedDict - Typing-sig - python.org

Allowing anonymous types may make sense in cases where a type is needed only once.
My propsal, however, does not rely on anoynomous types, just on local named type definitions, though.
Anyway, as tmk said below, a PEP for inline typed definitions seems to be under way.
Regaring my numbering of the local TypedDict definitions, consider this, more complex, example:

class NotebookDocumentSyncOptions(TypedDict):
    class NotebookSelector_0(TypedDict):
        class Cells_0(TypedDict):
            language: str
        notebook: Union[str, NotebookDocumentFilter]
        cells: NotRequired[List[Cells_0]]
    class NotebookSelector_1(TypedDict):
        class Cells_0(TypedDict):
            language: str
        notebook: NotRequired[Union[str, NotebookDocumentFilter]]
        cells: List[Cells_0]
    notebookSelector: List[Union[NotebookSelector_0, NotebookSelector_1]]
    save: NotRequired[bool]

Here, the numbering or, at any rate, different names are needed for referring to different local types in a type-union. (Field “notebookSelector”)

The example is a translation of this TypeScript-interface:

export interface NotebookDocumentSyncOptions {
	notebookSelector: ({
		notebook: string | NotebookDocumentFilter;
		cells?: { language: string }[];
	} | {
		notebook?: string | NotebookDocumentFilter;
		cells: { language: string }[];
	})[];
	save?: boolean;
}

I think that example is way less readable than moving the definition of NotebookSelector outside the TypedDict, it’s difficult to parse what the actual TypedDict is supposed to look like, at least with the anonymous version you can still see very well what part of the definition is an actual part of the structure and what part of it is just noise.

I would write that example like so, I find this way easier to parse as a human.

class NotebookCell(TypedDict):
    language: str

class NotebookSelectorByFilter(TypedDict):
    notebook: Union[str, NotebookDocumentFilter]

class NotebookSelectorByCells(TypedDict):
    cells: List[NotebookCell]

class NotebookSelectorByFilterAndCells(
    NotebookSelectorByFilter,
    NotebookSelectorByCells
):
    pass

NotebookSelector: TypeAlias = Union[
    NotebookSelectorByFilter,
    NotebookSelectorByCells,
    NotebookSelectorByFilterAndCells
]

class NotebookDocumentSyncOptions(TypedDict):
    notebookSelector: List[NotebookSelector]
    save: NotRequired[bool]

I like your code example very much. But I forgot to mention that the example stems from a context where Typescript Interfaces are automatically converted to Python TypedDict-Definitions (GitHub - jecki/ts2python: Python-interoperability for Typescript-Interfaces).

It would be difficult to write an automatic converter that yields code as beautiful and readable as handwritten code.

And automatic converter would more likely produce code like this:

class NotebookDocumentSyncOptions_NotebookSelector_0_Cells_0(TypedDict):
    language: str

class NotebookDocumentSyncOptions_NotebookSelector_0(TypedDict):
    notebook: Union[str, NotebookDocumentFilter]
    cells: NotRequired[List[NotebookDocumentSyncOptions_NotebookSelector_0_Cells_0]]

class NotebookDocumentSyncOptions_NotebookSelector_1_Cells_0(TypedDict):
    language: str

class NotebookDocumentSyncOptions_NotebookSelector_1(TypedDict):
    notebook: NotRequired[Union[str, NotebookDocumentFilter]]
    cells: List[NotebookDocumentSyncOptions_NotebookSelector_1_Cells_0]

class NotebookDocumentSyncOptions(TypedDict):
    notebookSelector: List[Union[NotebookDocumentSyncOptions_NotebookSelector_0,
                                 NotebookDocumentSyncOptions_NotebookSelector_1]]
    save: NotRequired[bool]

In my biased opinion, I’d prefer the version with the local typed dict definitions. However, what could really improve readbility - even for automatic conversion - would be an inline syntax as depicted in tmk’s post. So, I am very much looking forward to that upcoming PEP mentioned there.