Skip to content

Beartype 0.18.0: Dictionaries and Defaults at the Event Horizon of the Typing Hole

Compare
Choose a tag to compare
@github-actions github-actions released this 02 Apr 08:07
· 62 commits to main since this release

Beartype 0.18.0 whimsically gyrates around in balloon pants while chanting: "Can't touch this."


...even the token bald man in a smoking post-apocalyptic wasteland doesn't know

It's kinda creepy, honestly – and it's really getting on your nerves. Yes, we get it. You think you're hot junk, @beartype 0.18.0! Yet you can't help but admire the sweat that coats its body. Beartype 0.18.0 worked out for this release. It's buff now. It's tough now. It does things now. It does things you thought it did five years ago now. Beartype 0.18.0 finally caught up to your expectations. It even tried to exceed them. It didn't, of course. It couldn't. It's only a bear. It rarely bathes. And, anyway, your expectations were unreasonable.

But... it almost did. Prepare to have your expectations almost exceeded:

pip install --upgrade beartype

But what is @beartype 0.18.0? What does it do? Nuthing, huh? It's all just hollow hype and empty promises again, huh?

To answer that reasonable question, let's unreasonably back up with an extended monologue while the camera man slow pans across @leycec's baldpate. What was @beartype < 0.18.0? Why would anyone actually suffer install older @beartype releases? In the anachronistic words of an inconvenient acronym I just made up: scientific quality assurance (SciQA). So you wanna type-check...

We're agreed that @beartype < 0.18.0 was limited in scope. If you fit inside that scope, your codebase fits inside a backpack. Congratulations. It pays to be lean. But what if you have a real codebase? What if you wanted to actually type-check general-purpose Python containers outside that scope?

You use @beartype 0.18.0! That's right. We're finally type-checking general-purpose Python containers. But first...

A Bald Man in Yellow Tights Confronts a Murderous Cyborg Live on GitHub

Prepare your battle-hardened body and soul for the epic maelstrom of delivered features that follows by watching this malicious YouTube video! Just kidding. It's wholesome. Really. It's Saitama vs. Genos – surely humanity's crowning achievement. Praise be to Arifumi Imai for he has seen the countenance of many small gods and found them all sadly lacking.

@leycec always queues up Saitama vs. Genos when he needs to get hyped. Gonna groom the hair off a scary cat giving you the ugly fish-eyed thousand-yard death grimace? Saitama vs. Genos. Gonna remount your girlfriend's 64-core ThreadInfernoBurner CPU that's already sintered six motherboards into charred thermal paste in the wood shed out back that the sea walruses are rifling through? Saitama vs. Genos. When things get real, you just get realer. Saitama vs. Genos.


when your shoulders rip off their hinges, you just hope that t-shirt was disposable

And now...

GitHub Sponsors: They Scratch the Bear's Back. Now, The Bear Scratches Back.

This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers:

Thanks so much, masters of fintech.


The Masters of Fintech. That's who.

And now... the moment people I have never met have been waiting for.

Dictionaries: The Mapping is Not the Territory, Whatever That Means

Beartype 0.18.0 now deeply type-checks the first key-value pair of each dictionary (mapping) annotated by a dictionary (mapping) type hint in O(1) constant time with negligible constant factors. This means near-real-time with runtime overhead of at most ~1µs (i.e., one microsecond, one millionth of a second) per type-check. This includes all hints of the form:

  • dict[..., ...].
  • collections.defaultdict[..., ...].
  • collections.abc.Mapping[..., ...].
  • collections.abc.MutableMapping[..., ...].
  • collections.abc.OrderedDict[..., ...].
  • typing.DefaultDict[..., ...].
  • typing.Dict[..., ...].
  • typing.Mapping[..., ...].
  • typing.MutableMapping[..., ...].
  • typing.OrderedDict[..., ...].

This (...probably) also includes @wesselb's multiple-dispatch pièce de résistance Plum, which should now automatically multiple-dispatch across different kinds of dictionaries without @wesselb actually having to do anything. Let us choose to believe this optimistic prophecy I have delivered.

Beartype 0.18.0 does so (...effectively) recursively on arbitrarily nested combinations and permutations of those type hints. The proof is in the disgusting British blood pudding possibly named something like "toad-in-the-bear-hole":

from beartype import beartype
from collections.abc import Mapping, MutableMapping

@beartype
def go_bear(bear_bros_for_life: dict[
    int, Mapping[str, MutableMapping[bytes, bool]]]) -> None:
    print(bear_bros_for_life)

# This passes. A beautiful dictionary brings a tear to the eye.
go_bear({
    1: {
        'Beautiful bird;': {
            b'thou voyagest to thine home,': False,
        },
    },
})

# This fails! A horrible dictionary brings your app crashing to the ground.
go_bear({
    1: {
        'With thine,': {
            b'and welcome thy return with eyes': 1,
        },
    },
})

Type-checking violation messages even identify the exact key-value pair of arbitrarily complex pure-Python data structures responsible for those violations. The above example now helps you ruin your coworker's all-too-brief web app career by raising:

beartype.roar.BeartypeCallHintParamViolation: Function __main__.go_bear()
parameter bear_bros_for_life={1: {'With thine,': {b'and welcome thy return with eyes': 1}}}
violates type hint dict[int, collections.abc.Mapping[str, collections.abc.MutableMapping[bytes, bool]]],
as dict key int 1 value dict key str 'With thine,' value dict key bytes
b'and welcome thy return with eyes' value int 1 not instance of bool.

Beartype 0.18.0: I swear that looks more readable when you see it in person.


if my leg ever bends like that, please call for help

Defaults: Apparently, Beartype < 0.18.0 Didn't Bother Checking Those

...heh. So, funny story. Apparently, @beartype < 0.18.0 didn't bother type-checking the default values of optional parameters at early @beartype decoration time. Why even bother, right? Beartype < 0.18.0 trusted you against its better judgement. Beartype < 0.18.0 only type-checked the default values of unpassed optional parameters at late function call time; if you always passed optional parameters (or never even called functions that accept optional parameters), @beartype < 0.18.0 never type-checked their defaults. This is why your office luncheons always order take-out that tastes like plastic.

Beartype 0.18.0 conveniently overlooks the abject failings of the distant past by embracing a new normal that you always thought was happening. Now, it is. All default values are now type-checked at early @beartype decoration time – with one prominent exception we're about to get to.

Behold! Type-check defaults at decoration time or go home, @beartype 0.18.0:

from beartype import beartype

@beartype
def beartype_i_am_your_code_father(
    nooooooooooooo: int = 'Oh, you will be. You will be.') -> None: ...

Despite the offending (and clearly offensive) beartype_i_am_your_code_father() function not being called, the @beartype decorator now raises the expected type-checking violation at decoration time:

beartype.roar.BeartypeDecorHintParamDefaultViolation: Function
__main__.beartype_i_am_your_code_father() parameter "nooooooooooooo"
default value 'Oh, you will be. You will be.' violates type hint
<class 'int'>, as str 'Oh, you will be. You will be.' not instance of int.

Above, we wrote that:

All default values are now type-checked at early @beartype decoration time – with one prominent exception we're about to get to.

What "prominent exception," @beartype? What bald-faced lies are you trying to sell us now, @beartype?!

The prominent exception is forward references. When the type hint annotating an optional parameter contains one or more unresolvable forward references (i.e., references to types that have yet to be defined), the @beartype decorator just issues a non-fatal warning rather than raising a fatal exception. After all, there might be a real problem there – but there might also not be a real problem there. @beartype can't tell, because the type is undefined. So, @beartype just notifies you that something is up. The power is in your hands. The power was always in your hands. After all, you use @beartype: the QA Power Glove.™ ← awkwardly dated 80's moments

Arise! Ignore unresolvable forward references in optional parameters at decoration time, beartype 0.18.0:

from beartype import beartype

@beartype
def ive_seen_bugs(you_people_wouldnt_believe: 'TearsInTheGitter' = (
    'Attack one-liners on fire off the shoulder of GitHub.')) -> None: ...

...which merely issues this non-fatal warning:

BeartypeDecorHintParamDefaultForwardRefWarning: Function
__main__.ive_seen_bugs() parameter "you_people_wouldnt_believe"
default value 'Attack one-liners on fire off the shoulder of GitHub.'
uncheckable at @beartype decoration time, as forward reference
"TearsInTheGitter" unimportable from module "__main__".
  @beartype

Beartype 0.18.0: because QA in 2024 is so complicated that you're just passively nodding along in the vain hope that one of these nothingburgers will start making sense.


if you ever see the Japanese character for death suspended in the air outlined in red letters, @beartype probably can't help you anymore. still, it's worth a try

PEP 647: typing.TypeGuard + beartype.door.is_bearable(): It's BAAAAAAAAAACK

Beartype 0.18.0 resuscitates PEP 647-compliant typing.TypeGuard[T]-based type narrowing on the beartype.door.is_bearable() type-checker. This means that mypy now loves beartype.door.is_bearable() as much as you love your rabid scrappy dog that mauls the jaded postal worker every day:

from beartype.door import is_bearable  # <-- *NOW WITH THE POWER OF PEP 647*

def narrow_types_like_a_boss_with_beartype(lst: list[int | str]):
    if is_bearable(lst, list[int]):
        munch_on_list_of_integers(lst)  # <-- mypy now loooves this
    elif is_bearable(lst, list[str]):
        munch_on_list_of_strings(lst)   # <-- mypy love intensifies

def munch_on_list_of_strings(lst: list[str]): ...
def munch_on_list_of_integers(lst: list[int]): ...

This is entirely thanks to a mammoth dissertation-length dissection by Python's unassailable typing genius @asford (Alex Ford) on the intersection oh gods what does any of this mean anymore of runtime and static type-checking vis-a-vis the procedural statement-level hybrid runtime-static type-checker beartype.door.is_bearable(), the PEP 484-compliant @typing.overload decorator, and the PEP 647-compliant typing.TypeGuard[T] type hint. Please redirect all blame towards @asford. I barely understand anything anymore.

Sadly:

  • This does not extend to the comparable object-oriented beartype.door.TypeHint.is_bearable() method – which continues to not perform type narrowing. Due to deficiencies in @leycec's wobbly brain, only the procedural beartype.door.is_bearable() function currently performs type narrowing.

  • This may not extend to pyright. Although mypy appears to fully support this API, pyright appears to raise fatal errors that make no sense and suggest pyright has an equally wobbly brain:

    /home/leycec/py/beartype/beartype/door/_doorcheck.py
      /home/leycec/py/beartype/beartype/door/_doorcheck.py:209:5 - error: Overloaded implementation is not consistent with s>
        Function return type "TypeGuard[T@is_bearable]" is incompatible with type "bool"
          "TypeGuard[T@is_bearable]" is incompatible with "bool" (reportInconsistentOverload)
      /home/leycec/py/beartype/beartype/door/_doorcheck.py:209:5 - error: Overloaded implementation is not consistent with s>
        Function return type "TypeGuard[T@is_bearable]" is incompatible with type "bool"
          "TypeGuard[T@is_bearable]" is incompatible with "bool" (reportInconsistentOverload)
    2 errors, 0 warnings, 0 informations
    

What's "funny" about that is that:

  • All TypeGuard[...] type hints reduce to and are thus compatible with the standard bool type.

  • PEP 647 claims that the reference implementation of PEP 647 is (...waitforit) pyright:

    The Pyright type checker supports the behavior described in this PEP.

Guess it doesn't, huh? We cry wet crocodile tears for OO and pyright.

Beartype 0.18.0: @leycec cannot be held responsible for his own failings.


@leycec clutches a living-preserving cup of "matcha on the rocks."

PEP 613: typing.TypeAlias: It's Deprecated, But That's Okay, Because Literally Everything Is Deprecated

Does anyone even care about deprecations anymore? Everything in the standard typing package has now been deprecated. When everything is deprecated, nothing is deprecated. The desensitization is real and you no longer care.

Beartype 0.18.0 appreciates your growing sense of futility and vaguely uneasy apprehension of impending doom. After all, @beartype is the codebase built by a playlist of twelve continuous days of stoner caveman doom metal from Dorset. If it's got a reputable name like "Dopesmoker", "Dopethrone", "Shroomaroom", or "Satori", we probably resolved your issue to it without your consent. Bonus GitHub karma to the bear bro that names the album that starts with this well-intended life lesson:

Drop out of life with bong in hand!
Follow the smoke to the riff-filled land!

That's why @beartype now officially supports PEPs that are dead that nobody cares about anymore like PEP 613: typing.TypeAlias. Although deprecated by PEP 695 type aliases (e.g., type hints of the form type {alias_name} = {alias_value} under Python ≥ 3.12), PEP 613 type aliases are still widely prevalent throughout the open-source community. Specifically, @beartype now:

  • Emits an absurd deprecating warning for each PEP 613 type alias that spans five volumes of archaic dead-tree print like that long-lost Brandon Sanderson cyberpunk fantasy saga you always knew existed:

    BeartypeDecorHintPep613DeprecationWarning: PEP 613 type hint
    typing.TypeAlias deprecated by PEP 695. Consider either:
    * Requiring Python >= 3.12 and refactoring PEP 613 type aliases into
      PEP 695 type aliases. Note that Python < 3.12 will hate you for
      this: e.g.,
        # Instead of this...
        from typing import TypeAlias
        alias_name: TypeAlias = alias_value
    
        # ...just do this.
        type alias_name = alias_value
    * Refactoring PEP 613 type aliases into PEP 484 "typing.NewType"-based
      type aliases. Note that static type-checkers (e.g., mypy, pyright,
      Pyre) will hate you for this: e.g.,
        # Instead of this...
        from typing import TypeAlias
        alias_name: TypeAlias = alias_value
    
        # ...just do this.
        from typing import NewType
        alias_name = NewType("alias_name", alias_value)
    
    Combine the above two approaches via The Ultimate Type Alias (TUTA),
    a hidden ninja technique that supports all Python versions and static
    type-checkers but may cause coworker heads to pop off like in that one
    jolly Kingsman scene:
        # Instead of this...
        from typing import TypeAlias
        alias_name: TypeAlias = alias_value
    
        # ..."just" do this. If you think this sucks, know that you are not alone.
        from typing import TYPE_CHECKING, NewType, TypeAlias  # <-- sus af
        from sys import version_info  # <-- code just got real
        if TYPE_CHECKING:  # <-- if static type-checking, then PEP 613
            alias_name: TypeAlias = alias_value  # <-- grimdark coding style
        elif version_info >= (3, 12):  # <-- if Python >= 3.12, then PEP 695
            exec("type alias_name = alias_value")  # <-- eldritch abomination
        else:  # <-- if Python < 3.12, then PEP 484
            alias_name = NewType("alias_name", alias_value)  # <-- coworker gives up here
  • Otherwise ignores each PEP 613 type alias, which conveys no meaningful semantics or metadata. Frankly, it's unclear why PEP 613 even exists. The CPython developer community felt similarly, which is why PEP 695 type aliases deprecate PEP 613.

Beartype 0.18.0: it's not our fault.


gettin' kinda queasy watchin' cyborg man spin around like an unsafe carousel on fire

Warning and Exception Messages: They No Longer Blow Quite As Much Chunks

They still blow chunks, of course. @beartype gonna @beartype. But at least warning and exception messages emitted by @beartype no longer blow quite as much chunks. I'll take it.

Specifically:

  • Type-checking violations now sport a (slightly) more coherent colour scheme. Let me know if you hate it. Monochrome lovers gonna hate, of course. I (probably) won't do anything about your justifiable complaints, of course. I'm unresponsive, mentally flabby, and lack fortitude. I'll just quietly accept your abuse. But at least one of us might feel better.

  • The beartype.door.beartype_this_package() import hook (i.e., the rising child star of the @beartype ecosystem that our sketchy PR agency has been spamming your inbox with) now raises exception messages like this when called from:

    • Top-level scripts:

      beartype.roar.BeartypeClawHookUnpackagedException: Top-level script
      "/home/leycec/tmp/src/script.py" resides outside package structure.
      Consider calling another "beartype.claw" import hook. However, note that only
      other modules will be type-checked. "/home/leycec/tmp/src/script.py" itself
      will remain unchecked. All business logic should reside in submodules
      subsequently imported by "/home/leycec/tmp/src/script.py": e.g.,
          # Instead of this at the top of "/home/leycec/tmp/src/script.py"...
          from beartype.claw import beartype_this_package  # <-- you are here
          beartype_this_package()                          # <-- feels bad
      
          # ...pass the basename of the "src/" subdirectory explicitly.
          from beartype.claw import beartype_package  # <-- you want to be here
          beartype_package("src")  # <-- feels good
      
          from src.main_submodule import main_func  # <-- still feels good
          main_func()                   # <-- *GOOD*! "beartype.claw" type-checks this
          some_global: str = 0xFEEDFACE  # <-- *BAD*! "beartype.claw" ignores this
      This has been a message from your friendly neighbourhood bear.
    • Top-level modules:

      Top-level module "main.py" resides outside package structure but was
      *NOT* directly run as a script. "beartype.claw" import hooks require
      that modules either reside inside a package structure or be directly
      run as scripts. Since neither applies here, you are now off the deep
      end. @beartype no longer has any idea what is going on, sadly.
      Consider directly decorating classes and functions by the
      @beartype.beartype decorator instead: e.g.,
          # Instead of this at the top of "main"...
          from beartype.claw import beartype_this_package  # <-- you are here
          beartype_this_package()                          # <-- feels bad
      
          # ...go old-school like it's 2017 and you just don't care.
          from beartype import beartype  # <-- you want to be here
          @beartype  # <-- feels good, yet kinda icky at same time
          def spicy_func() -> str: ...  # <-- *GOOD*! @beartype type-checks this
          some_global: str = 0xFEEDFACE  # <-- *BAD*! @beartype ignores this, but what can you do
      For your safety, @beartype will now crash and burn.
  • Prefixes all warning messages with contextual metadata describing the origin of those warnings. This includes the names of the responsible module, class, and/or callables that are rapidly sapping your will to code even a single line more. Notably:

    • PEP 585 deprecations, in particular, were particularly useless. They failed to notify you where exactly the deprecated type hint came from. Now, @beartype candidly tells all about childhood trauma on the Christopher Robin farm:

      from beartype import beartype
      from typing import List  # <-- deprecated is bad, but that doesn't mean you care
      
      @beartype
      def when_the_going_get_buggy(the_buggy_get_beartype: List[str]) -> None: ...

      ...which now raises a deprecation warning that actually helps you succeed on your own merit for once:

      BeartypeDecorHintPep585DeprecationWarning: Function
      __main__.when_the_going_get_buggy() parameter "the_buggy_get_beartype"
      PEP 484 type hint typing.List[str] deprecated by PEP 585. This hint is
      scheduled for removal in the first Python version released after October
      5th, 2025. To resolve this, import this hint from "beartype.typing"
      rather than "typing". For further commentary and alternatives, see also:
          https://beartype.readthedocs.io/en/latest/api_roar/#pep-585-deprecations
    • PEP 526-compliant annotated variable assignments (e.g., muh_var: int | str = True), for which beartype.claw import hooks raised similarly useless type-checking violation messages. Thankfully:

      • Global annotated variable assignments like this:

        bad_global: str = 0xFEEDFACE

        ...now yield violations like this:

        beartype.roar.BeartypeDoorHintViolation: Global variable
        "__main__.bad_global" value 4277009102 violates type hint <class
        'str'>, as int 4277009102 not instance of str.
      • Local annotated variable assignments like this:

        def bad_func():
            bad_local: int = "This local is bad. It's bad. It knows it."
        
        bad_func()

        ...now yield violations like this:

        beartype.roar.BeartypeDoorHintViolation: Callable
        paga.__main__.bad_func() local variable "bad_local" value "This local
        is bad. It's bad. It knows it." violates type hint <class 'int'>, as
        str "This local is bad. It's bad. It knows it." not instance of int.

Beartype 0.18.0: raise your paws in the air like you just don't care.


that feeling when you replace your hand with a black-and-white Ultima Cannon and it's still not good enough

I Am Very Tired and I Must Now Lie Down

Beartype 0.18.0 salutes the bear bros and gals and cats and rabid scrappy dogs that made this miracle possible:

@posita, @patrick-kidger, @wesselb, @tusharsadhwani, @JWCS, @iamrecursion, @rbroderi, @tvdboom, @AlanCoding, @crypdick,← lol @komodovaran, @rwiegan, @avolchek, @jaanli, @brettc, @spagdoon0411, @helderco, @jamesbraza, @dcharatan, @kasium, @uriyasama


Standard @beartype training exercise: all fun and games until you're down to the last chip.