Proposed initial typing spec

Now that the Typing Council (PEP 729) is in place, it’s time to start executing. For clarity: I am not writing this post on behalf of the Council.

The first thing I’d like to get done is to officially adopt the initial spec I put together. It is in its own repo and I put it up on ReadTheDocs:

https://draft-typing-spec.readthedocs.io/en/latest/spec.html

The spec was created almost entirely by copy-pasting the specification sections from past typing PEPs, with light editing to remove clearly out of date content and harmonize the text. As discussed in the previous PEP 729 threads, I think this is the best way to get started. After this initial text is adopted, the Typing Council will be able to consider proposals to change the spec.

At this point, I’m primarily looking for two kinds of feedback:

Ways to improve presentation

For example, should the organization into top-level sections be different? A good organization will make it so that future improvements ideally touch only a few sections at a time, making them easier to understand and review.

Should the spec be one (huge!) document, or a set of multiple pages? The document is about 35k words long, more than twice the length of the longest PEP. However, rendering the whole thing as a single page makes it easier to use in-browser search on the whole document.

Are there any improvements to be made to wording? @rchen152 just contributed a number of fixes for places where the spec used wording like “this PEP”, but maybe there’s more.

Ideas for future content improvements

For simplicity, I want the initial spec to follow the existing PEPs precisely in terms of actual content. However, the PEPs are not a great spec: they are uneven, often imprecise, and sometimes out of line with what was actually implemented in type checkers.

This is a good opportunity to look for places where the spec can be improved. I filed a few already as issues on my repo, and I’m sure there are many more possible areas for improvement.

The Council will likely have a lot of these decisions to make over the next few months. We’ll have to decide internally how to prioritize the work if there are multiple proposals that need our decision. However, I would prefer to focus on those topics that are most important to the community.

Next steps

We’re still putting some of the logistics together, but the expectation is that we’ll create a typing-council repo on GitHub where issues can be opened to request the Council to make a decision. Once this thread has run its course and the logistics are in place, I’ll open an issue to request the Council’s formal decision to adopt the initial spec.

19 Likes

This is mostly a nitpick from reading the first few subsections and probably not that important, considering the target audience for the specification and it’s probably an artifact of examples being lifted directly from the originating PEPs, although since the specification may serve as the basis for a more end user facing document this may still be worth looking out for:

I have noticed that the examples sometimes use features/constructs that have not yet been introduced at this point in the specification and sometimes even use them in ways that are discouraged/less common. Generally I think the examples should try to follow best practices and change as they evolve.

Look at e.g. the 4th/5th example in the type aliases section: Typing Specification — Draft Typing Specification documentation

It’s making use of TypeVar before it has been mentioned even once and instead of defining an upper bound, which is usually what is encouraged, it uses the enumeration of valid types approach, which can lead to surprising results (such as disallowing a union of two valid types, without explicitly adding it to the enumeration).

Would you want us to report nit-picky issues like that or is that something that should be left for later?

I’ve also noticed that the formatting of examples is somewhat inconsistent, maybe we could leverage black to auto-format the examples?

2 Likes

Very very strong +1 to this part of the approach.

In many cases, the PEPs could be refined to match minor improvements. I think this process will support doing that in a mindful way.
Idea: label issues with the PEP numbers for easy categorization when/if folks want to backport improvements.

To put that number in perspective, 35K words is a novella. I just checked and “Of Mice and Men” is 30K.
I think a document of this length should have pages.

I’m not sure where the best page breaks are offhand (“TypeVar and related constructs” seems like a logical page?), but I think a small number of pretty big pages would be a way to start.

1 Like

Thanks for the feedback!

I agree that the examples should use best practices; I already made a lot of changes like Listlist. I noticed in the section you linked (about type aliases) that I missed some Type[T] instances, so those still need to be fixed.

I don’t know if it’s worth to avoid using concepts in examples that have not yet been used. It seems difficult to do in all cases, and the audience for the spec should have a general familiarity with typing concepts already. The user-facing documentation is a separate project and need not follow the same structure.

Yes, feel free to report these. Note that the spec will be moved to a different repo (python/typing) eventually, so any CI setup would also need to be transferred later.

PEPs are meant to be historical documents that shouldn’t change after they’re implemented. This process is meant to reduce the reliance on old PEPs in the typing community. The PEPs we incorporate should get a banner pointing to the typing spec as the up-to-date canonical documentation.

I feel the logical choice is to make each of the top-level sections its own page. However, “Type system features” might need further splitting.

2 Likes

I understand that’s the general rule about significant content changes – i.e. PEPs aren’t living specifications – but isn’t there enough space in the amendment process to incorporate changes which merely clarify behavior? Or maybe that’s too close to removing existing ambiguities from a spec.

1 Like

Thanks for the initial cut at this spec, Jelle. This is great progress!

I agree with many of the other comments already posted. The current draft reads as though it’s a bunch of documents pasted together — because that’s what it is :slight_smile: .

I think that the order of the topics will need to change and the sections will need to become more fine-grained over time. We could either attempt to do that upfront or do it as a second pass. My (slight) preference is to do it upfront, but I’m OK with either approach. I’ve taken a stab at a finer-grained organizational structure, which I’ve included below. I’ve attempted to order the sections such that they build on each other. My proposed organization includes some bullet items and sections that are not currently covered in any PEP but should probably be added over time to make the typing spec complete. We could leave these as placeholders in the initial draft if we adopt my proposal.

I think we should break it into multiple documents, It’s already unwieldy, and I think it’s going to grow (perhaps significantly) over time as we add clarifications and document new typing features. My thought is that each section (chapter?) should be a separate document. This will make it easier to use as a reference. It will also facilitate PR reviews as the spec is updated. We could solve the searchability problem by adding a simple search mechanism on the documentation website. Links will also facilitate navigation between sections.

Here’s my proposed organization:

The Python Type System

  • What is a type system?
  • Static (vs runtime) type checking
  • Goals & non-goals
  • Purpose and organization of this spec
  • Definition of terms

Type System Concepts

  • Type declarations
  • Type evaluation & inference
  • Subtype relationships
  • Unions
  • Gradual typing
  • Nominal vs. structural typing

Type Annotations

  • Meaning of annotations
  • Valid type expression forms
  • Forward references
  • Scoping of annotations
  • Runtime evaluation of annotations
  • Function & variable annotations
  • Generator functions and awaitables

Special Types in Annotations

  • Any
  • None
  • Callable
  • tuple
  • float and complex (special-case type promotions)
  • NoReturn & Never

Metaclasses

  • Class objects vs. class instances
  • Metaclass hierarchy and type

Generics

  • Type parameters
  • Type parameter scoping
  • Generic class definitions
  • Generic function definitions
  • Specialization
  • Constraint solving
  • Variance
  • TypeVar
  • Upper bounds
  • Constrained TypeVars
  • ParamSpec & Concatenate
  • TypeVarTuple & Unpack
  • Self
  • Variance inference

Type Qualifiers

  • final
  • Final
  • Annotated

Class Type Compatibility

  • Liskov substitution principle & LSP exemptions
  • Override checks
  • Abstract classes & methods
  • Multiple inheritance
  • Class-scoped variables & ClassVar
  • override

Type Aliases

  • Runtime evaluation of type aliases
  • TypeAlias
  • type statements
  • Generic type aliases
  • Recursive type aliases
  • NewType

Literals

  • Literal expressions
  • Literal
  • LiteralString

Protocols

  • Overview of protocols & Protocol
  • Default implementations in protocols
  • Protocol matching for classes, types, and modules

Callables

  • Positional-only parameters
  • Keyword-only parameters
  • Argument defaults
  • Annotating *args and **kwargs
  • Callable
  • Callback protocols
  • TypeVarTuple and *args
  • Callable subtyping rules

Overloads

  • overload
  • Overload matching for calls

Methods

  • Instance, class, and static methods
  • Binding a class or type to a method
  • Class constructors

Descriptors

  • Descriptors
  • Properties
  • Asymmetric descriptors

Dataclasses

  • dataclass and type checking
  • dataclass_transform

Typed Dictionaries

  • TypedDict and type checking
  • Required and NotRequired

Enum

  • Enum and type checking

NamedTuple

  • NamedTuple and type checking

Type Narrowing

  • Narrowing on assignment
  • Narrowing rules for Any
  • Built-in type guards
  • TypeGuard

Type Checker Directives

  • assert_type
  • reveal_type
  • # type: ignore comments
  • cast
  • TYPE_CHECKING
  • no_type_check
  • type_check_only
  • deprecated

Distributing Type Information

  • Stub files
  • Type information in libraries
  • Import Resolution Ordering

Historical & Deprecated Features

  • Capitalized forms of builtins exported form typing module
  • typing_extensions module
  • Optional
  • Union
  • Type comments
13 Likes

Yes, it will probably be difficult to ensure that all examples only use concepts that have already been introduced, especially while things are still being moved around and reordered, so it’s probably not worth trying too hard there.

My point was more that if we were going to use an unrelated feature in an example, we should probably stick to the most simple usage unless it illustrates something important specific to the example, i.e. in the aforementioned case we should use TypeVar("T") instead of TypeVar("T", int, float, complex) since whether or not the TypeVar is constrained contributes nothing to the complexity of the type alias, it’s just unrelated noise.

This also avoids accidentally communicating that this is a common pattern, when in reality it is really very rare you want to use a constrained TypeVar over a bounded one, so it should probably only really show up in the section that is talking about TypeVar.

4 Likes

Thanks, I think that organization makes sense. It’s better to make these changes now, so that future substantive changes to the spec are easier.

A few nit picks:

I’d accept a PR implementing this proposed reorganization in my repo, or I may give it a try myself in the next few days.

2 Likes

Would it make sense to group Literals, Enums and Unions together somehow? As sum types of increasing complexity and versatility.

What makes @final a “type qualifier” while @deprecated is a “type checker directive”

Here’s my thinking. @final affects type evaluations and @deprecated does not. The absence or presence of the latter will never change whether a statement is valid from the perspective of the type system.

“Historical & Deprecated Features” isn’t a perfect heading for typing-extensions

Fair enough. It’s typically used for compatibility, so it felt related. Maybe the word “compatibility” should appear somewhere in this section title.

Where would we add rules for how type checkers should treat __exit__

Good question. I’m not sure. Maybe we need another section for things like that. That’s probably what you had in mind with your “Interaction with Python features” section?

Would it make sense to group Literals, Enums and Unions together somehow?

I don’t see how those have much in common. They’re very different concepts from a typing perspective.

3 Likes

Thank you, Jelle!

Conceptually they are the exact same concept: union types. In fact, I should have added bool and None to the list. (None isn’t a sum type per se but 99% of uses of None is to create union types, so a strongly related concept.)

All of these are conceptually equivalent:

bool # Effectively a Literal[False, True] with `__call__` being equal to Callable[[Any], Literal[False, True]]

type BoolLiteral = Literal["false", "true"]

class BoolEnum(Enum):
    false = 0
    true = 1

type BoolUnion = Literal["false"] | Literal["true"]

You could argue that these have very different histories and Python devs are used to using them in different contexts; and sure. They also have very different ergonomics and affordances. But… logically they’re siblings.

Edit: replace ‘sum type’ with ‘union type’ for technical correctness.

2 Likes

None of these are sum types. The closest to being one is a union, but sum types have a definition and unions aren’t them without more than just union.

Literals are closer to refinement types than sum types.

I don’t think muddying the water by saying “Well this is like this other type” is useful when the goal is a precise specification.

First of all, thank you for getting this started.

After reviewing what’s here so far, I’m not sure the word “specification” should be used to describe this. At least not yet. It is essentially just a unification of all of the documents that were considered too loosely defined to provide a specification. I have nothing against adopting a unified document like this which should be a living document and refined over time, but I do think that describing it as a specification may give the wrong impression about the state of it.

Sorry this may seem like a nit, but I hope that by starting with what’s here, we can look for holes in what is well defined (even if we just default to current behavior by consensus of type checkers where it exists), clear it up, and then promote it to a full specification once any deficient specifications are addressed. That would get us to the “here’s what we have already agreed to define” stage where any further improvements or changes could be argued while showing what they would add/improve/etc in the context of the whole.

I also share Eric’s concerns about this being unwieldy as-is, but have only one thing to add to that:

Something I don’t think is adequately covered is what parts of the type system should be composable with others. Even with the suggested reorganization, this might benefit from its own section with either a table or a list of things that are intentionally restricted from composing.

1 Like

Great list! This makes me think, would it be worth mentioning the typing directive “type_check_only”?
I feel like it is severely underused in stubs where a class (Protocol, TypedDict, something hidden in C implementation,
…) is defined but doesn’t exist at runtime.

1 Like

I just reorganized the spec according to your suggestions in Reorganize the spec by JelleZijlstra · Pull Request #16 · JelleZijlstra/typing-spec · GitHub. Overall, I think this organization works well. As you mentioned, many important sections are still missing. I am leaving those out for now, but we could add some placeholders.

A few specific notes:

  • Your outline seems to suggest covering type[] in the section on metaclasses, but that feels inappropriate to me; for now I’m putting it in “Special types in annotations” instead.
  • I left positional-only arguments syntax (double underscores) in “Historical features”, as it’s a compatibility hack for Python <3.8. Your outline suggests sections about pos-only and kw-only parameters, but I’m not sure we need them: type checkers should simply support this part of the language.
  • Not clear where you’d want Unpack on kwargs (PEP 692); I’m putting it in “Callables”. (This section of the spec can likely be shortened, but that’s for later.)
  • NamedTuple and Enum are not really specified in the current PEPs, so I am omitting them. These sections should be written from scratch in the future. (They could probably go in the “Python features” section if we add that, to reduce the number of top-level sections.)
  • @type_check_only and @deprecated aren’t yet covered.
  • Wasn’t sure where to put the section on sys.platform/sys.version_info, so I put it in “Type checker directives”.

This splits the document into 18 pages. We may add more later for a few sections in your outline that are currently missing. The longest section at the moment is the one on Generics at just over 10k words.

In my reorganization PR, I added some verbiage to the introduction acknowledging that the spec is incomplete and many improvements are needed. I do think the word “specification” is appropriate even at this stage: after all, it was extracted from PEP sections titled “Specification”. An imperfect spec is still a spec.

What kind of features did you have in mind here? My general reaction is that composability should be addressed throughout the document where needed. For example, we might define a term like “type expression” to include all the forms that are generally acceptable in types. Then the section on e.g. ClassVar would say that the expression inside the brackets should be a type expression, and that annotations on class variables can use ClassVar in addition to standard type expressions. With such an approach, composability flows naturally from the text of the spec.

1 Like

acknowledging it as incomplete serves the same purpose as my suggestion here, thank you.

This part is a bit complex. There’s a difference between what wasn’t specified and wasn’t implemented as composable vs things that were explicitly restricted in the proposal themselves. Paramspec has several intentional restrictions on it in its current state. A recent example of the other I came across, the spec for TypedDicts doesn’t specify that type checkers should synthesize overloads for bound methods, leading to a case where these can’t be used in callbacks, key functions, etc and retain type information.

Pyright determined that because mypy was the reference implementation for this and that the reference implementation did not do it, it should not be done. I’d personally argue this is an unnecessary hole in the type system due to insufficient specification and an implementation that could have done more, but was it actually intentional that this was not specified? There’s no indication of this that I can find in the original pep.

I have now posted the spec as a PR to the typing repo and requested the Typing Council to make a decision. Once the Council approves the document, we can merge it and start making substantive improvements.

Of course, suggestions for textual improvements are still welcome.

One reply:

Thanks for the concrete examples. These sound like things we can address one by one as needed, so I don’t think it affects the current initial spec. We can look into extending the spec for TypedDict after the initial spec is in place.

3 Likes