Skip to content

Inline type annotations

Richard Marmorstein edited this page Oct 27, 2023 · 6 revisions

Migration guide: inline type annotations

Summary

In stripe-python v7.1.0, we enabled inline type annotations. These annotations describe the shape of the Stripe API much more precisely than the type stubs available in Typeshed, which is good news and bad news.

Benefits

The good news is that, if you use a type checker like MyPy or Pyright, or a types-enabled IDE, you will now be able to use stripe-python with much more confidence.

  • The types will warn you if you attempt to send or access a field that does not exist.
  • Autocomplete will allow you to explore what API methods are available, and what data appears on the responses.
  • Field-level docstrings have been added, so you can discover the meaning of your favorite API fields from the comfort of your IDE.
  • When you upgrade Stripe, type errors will notify you if a field has been removed or deprecated in the latest version.

Challenges

The bad news is, when you upgrade to v7.1.0, your type checker may begin to emit more type errors as it begins to validate your integration against the new, more specific types. The inline types supplant the stubs that were available from typeshed, which were very lenient.

Causes of type errors

If you upgrade to or beyond v7.1.0 and see new errors, there are several possible causes

  • ** Your integration may have a logic error**. Perhaps you have a typo in a field name, or are using a value incorrectly? Alerting you to mistaken code is the main value proposition of types. This is the welcome kind of type error.

  • Your integration may be old or non-standard. The type annotations in stripe-python correspond to the most recent API version (at the time of release). By default, stripe-python uses this version when it sends requests. But if you are overriding the default and specifying a specific version other than the libraries, or if your webhooks are configured to older api versions, the types in the library might not agree with the types you experience at runtime. A mismatch can also happen if you are using undocumented fields. Perhaps you are enrolled in a beta program that gives you access to fields that aren't public yet.

    The best option for resolving type errors in this category is to upgrade your integration to use the latest API version, or see if a documented alternative exists. If not, you can suppress the errors with a comment like # type: ignore.

  • Your type checker may require additional proof of safety

    For example, suppose you have code like

    def print_email(s: str):
        print(s)
    
    print_email(stripe.Customer.retrieve("xyz").email)

    This code does not produce a type error with the lenient types from Typeshed, but after v7.1.0 it will produce a type error Argument of type "str | None" cannot be assigned to parameter "s" of type "str" in function "print_email". The type of Customer.email is Optional[str]. Not all Stripe Customer resources have an email, and so the type checker complains if you try to print the email without checking to see if it actually exists. It very well may be that in your Stripe integration, all customers have an email, but the type checker won't be able to pick up on this fact. In fact, the type checker will even refuse to accept print_email(stripe.Customer.create(email="i.have.an.email@example.com").email).

    To address these type errors, you can use assertions to narrow the types, see the mypy documentation. In this example:

    cus = stripe.Customer.retrieve("xyz")
    assert cus.email is not None
    print_email(cus.email)

    You can also suppress the type errors with a comment

    cus = stripe.Customer.retrieve("xyz")
    print_email(cus.email)  # type: ignore

    One thing to note, methods are typed using Unpack[TypedDict[...]]. This works well if you are providing all parameters to the method inline, but if you wish to define the params dict separately and then spread that into the method, you may need to give the type checker a hint. For example,

    # no error
    cus = stripe.Customer.create(email="monty@python.org")
    
    # type error
    params = {"email": "monty@python.org"}
    cus = stripe.Customer.create(**params)
    
    # no error
    params: stripe.Customer.CreateParams = {"email": "monty@python.org"}
    cus = stripe.Customer.create(**params)

Suppressing type errors

The easiest way to suppress type errors is with a comment like # type: ignore as demonstrated, but wholesale error suppression is kind of a blunt instrument. If you use the Pyright type checker, you can comment # pyright: ignore[specificDiagnostic] to more specifically suppress any of the particular error categories that pyright features.

If you wish to change some of stripe-python's types to better describe your usage, instead of suppressing them you can override them by writing your own stub .pyi files for stripe-python. (If you use Pyright, you can put them under typings/stripe and they should get picked up by default. If you use MyPy you have to add the path where they are located to the MYPYPATH environment variable, see PEP 561. Writing stubs isn't as easy as writing suppression comments, but it does allow for greater type safety, if your types are accurate.

If you prefer the old, lenient stubs vs. the new inline type annotations, run pip install types-stripe to install the pypi stubs package. This package contains the same stubs that are present in the Typeshed repository, but installing them explicitly as a package will give them a higher precedence than the inline types (see PEP 561). This is only a temporary measure, though. Typeshed stubs are typically removed six months after a package gains inline type annotations.

Versioning Policy

We release type changes in minor releases. While stripe-python follows semantic versioning, our semantic versions describe the runtime behavior of the library alone. Our type annotations are not reflected in the semantic version. That is, upgrading to a new minor version of stripe-python might result in your type checker producing a type error that it didn't before. You can use a ~=x.x or x.x.* version specifier in your requirements.txt to constrain pip to a certain minor range of stripe-python.

This is similar to the versioning policy we established for our Typescript types, and we are very open to feedback. Please open a Github issue if you have thoughts!