Package: overtake
OvertakeHow to install?pipinstallovertakeOvertake is quite self-contained. The only dependency, by default, istyping-extensions.What is Overtake?Have you ever dreamed of declaring the same function multiple times?defcount_words(arg:str)->int:returnlen(arg.split())defcount_words(arg:list[str])->int:returnsum(len(text.split())fortextinarg)print(count_words("one two three!"))# 3print(count_words(["one two","three four five six"]))# 6Well you are at the right place!Overtake is a small library made to push@typing.overloadeven further.@typing.overloadjust defines signatures, so that type checkers know what type hints
are available when calling a function.
With Overtake, you can also call those functions at runtime, similar to languages like
C++ or Julia.An example is worth a thousand words:fromtypingimportoverload# if you are using Python <3.11, you'll need instead# from typing_extensions import overloadfromovertakeimportovertake@overloaddefcount_words(arg:str)->int:returnlen(arg.split())@overloaddefcount_words(arg:list[str])->int:returnsum(len(text.split())fortextinarg)@overtake(runtime_type_checker="beartype")defcount_words(arg):...print(count_words("one two three!"))# 3print(count_words(["one two","three four five six"]))# 6Overtake will analyse the types and provided arguments to call the right implementation.It works for every signature supported by@typing.overloadThis pattern is supported by IDEs (Pycharm, VSCode, etc...) so autocompletion will work well.
It's also supported as well by type checkers (Mypy, Pyright, etc...) so you don't need to compromise on type safety 😁Overtake follow closely the Mypy guide on@typing.overload:https://mypy.readthedocs.io/en/stable/more_types.html#function-overloadingMore advanced examples.We can show you here more pattern that are possible. Basicallyif isinstance(..., ...)might be your cue thatovertakemight help you write clearer code.RecursivityLet's write a function that returns the number of days since January 1st:fromtypingimportoverloadfromdatetimeimportdatefromovertakeimportovertake@overloaddefdays_this_year(current_date:date)->int:delta=current_date-date(2023,1,1)returndelta.days@overloaddefdays_this_year(current_date:str)->int:returndays_this_year(date.fromisoformat(current_date))@overtakedefdays_this_year(current_date):...print(days_this_year(date(2023,8,15)))# 226print(days_this_year("2023-08-15"))# 226You can call your function in a recursive manner, to deduplicate code. We could actually rewrite our first example like this:fromtypingimportoverloadfromovertakeimportovertake@overloaddefcount_words(input_value:str)->int:returnlen(input_value.split())@overloaddefcount_words(input_value:list[str])->int:returnsum(count_words(text)fortextininput_value)@overtake(runtime_type_checker="beartype")defcount_words(input_value):...print(count_words("one two three!"))# 3print(count_words(["one two","three four five six"]))# 6Different output typesIt's also possible to have different output types, like with@overloadfromtypingimportoverloadfromovertakeimportovertake@overloaddefconvert_to_int(input_value:str)->int:returnint(input_value)@overloaddefconvert_to_int(input_value:list[str])->list[int]:return[int(x)forxininput_value]@overtake(runtime_type_checker="beartype")defconvert_to_int(input_value):...print(convert_to_int("88"))# 88 (an integer)print(convert_to_int(["88","42","84"]))# [88, 42, 84] (a list of integers)Leveraging optional argumentsIt can avoid some annoying uses ofif ... is None:, you can specify different number of arguments (but the order must match!).For this example, let's say that you want the user to be able to write some text in any kind of file.
If the file is not provided, we create a temporary file.
We must accept any input for the file. Astr, apathlib.Path, a file-like object too. Or nothing (random file).fromtypingimportoverloadfrompathlibimportPathimportioimportrandomfromovertakeimportovertake@overloaddefwrite_text_to_file(text:str,file:io.TextIOBase)->None:file.write(text)@overloaddefwrite_text_to_file(text:str,file:Path)->Path:file.write_text(text)returnfile@overloaddefwrite_text_to_file(text:str,file:str)->Path:returnwrite_text_to_file(text,Path(file))@overloaddefwrite_text_to_file(text:str)->Path:random_file_name=f"/tmp/{random.randint(0,10)}.txt"returnwrite_text_to_file(text,random_file_name)@overtake(runtime_type_checker="beartype")defwrite_text_to_file(text,file=None):...print(write_text_to_file("hello world"))# /tmp/4.txtprint(write_text_to_file("hello world","/tmp/some-file.txt"))# /tmp/some-file.txtprint(write_text_to_file("hello world",Path("/tmp/some-file.txt")))# /tmp/some-file.txtprint(write_text_to_file("hello world",io.StringIO()))# None (we didn't write in a file on disk)Leveraging keyword argumentsYou can use this paradigm to force the user to use less arguments than the number defined in the function.
Here is an example where you're looking for the balance of a user. You can provide the user id, or the user's name,
but not both.fromovertakeimportovertakefromtypingimportoverload@overloaddeffind_user_balance(name:str)->int:return40@overloaddeffind_user_balance(user_id:int)->int:return50@overtakedeffind_user_balance(*,user_id=None,name=None):...print(find_user_balance(name="Julie"))# 40print(find_user_balance(user_id=14))# 50In this situation, Overtake saves you quite some time. You don't have to check thatuser_idandnameare both provided, and don't have to check that none of them has been provided.
Overtake does this for you!RecommendationsWe recommend using a type checker of your choice (Mypy, Pyright, etc...) so that the type checker catches
invalid usages of@overload. Even though it's not mandatory, it's helpful to catch mistakes with@overloadearly.The argumentruntime_type_checkerRuntime type checking in Python is difficult. Actually, very difficult. A few libraries provide this functionality, likebeartypeorpydantic.To avoid having to install any dependency,overtakecan useisinstanceas the default"basic"type checker.
Note that this will only work with types that are classes. For example, it won't work withlist[str]but it will work withlistordatetime.To handle more complicated types, you should use@overtake(runtime_type_checker="beartype")deffind_user_balance(*,user_id=None,name=None):...or@overtake(runtime_type_checker="pydantic")deffind_user_balance(*,user_id=None,name=None):...Whatruntime_type_checkerdo I need? There are so many choices!!!First of all, don't pick any ("basic"will be used by default) and see if it works.
If the types you are using are too complicated (if you are using generics or protocols),
overtake will raise an error and tell you to use anotherruntime_type_checker.Then you have the choice between"beartype"and"pydantic". Ask yourself the question,
"Do I need beartype-specific types? Likebeartype's validators?"
If yes, use beartype.If not, ask yourself the question
"Do I need Pydantic's specific types? LikePydantic's urls or custom types?"
If yes, use pydantic.If you are still undecided after this, you should use beartype as it's faster (validate in O(1)), but beware,
you might get silent errors for unsupported types, and it might be really unpleasant.
Seethis issue.
Pydantic might be slower, but you'll get errors when using an unsupported type. So it's safer.Both are really cool libraries, I encourage any curious mind to go read those docs!
We recommend you install those libs withpip install overtake[beartype]orpip install overtake[pydantic].What cool stuff can I do with the Beartype type checker?I was waiting for you to ask. Lo and behold!fromovertakeimportovertakefromtypingimportoverload,Annotatedfrombeartype.valeimportIs# Type hint matching only strings with lengths ranging [4, 40].LengthyString=Annotated[str,Is[lambdatext:4<=len(text)<40]]# Type hint matching only strings with lengths ranging [0, 4].ShortString=Annotated[str,Is[lambdatext:0<=len(text)<4]]@overloaddefis_this_string_big(arg:ShortString)->str:return"This is a short string!"@overloaddefis_this_string_big(arg:LengthyString)->str:return"This is a very long string"@overtake(runtime_type_checker="beartype")defis_this_string_big(arg):...print(is_this_string_big("Hi!"))# This is a short string!print(is_this_string_big("No one expects the spanish inquisition!"))# This is a very long stringDo you even needifstatements anymore? One can wonder.What cool stuff can I do with the Pydantic type checker?You can have fun with their custom types!fromtypingimportoverloadfromovertakeimportovertakefrompydanticimportMongoDsn,RedisDsn@overloaddefconnect(arg:RedisDsn)->str:return"Connected to redis!"@overloaddefconnect(arg:MongoDsn)->str:return"Connected to MongoDB!"@overtake(runtime_type_checker="pydantic")defconnect(arg):...print(connect("rediss://:pass@localhost"))# Connected to redis!print(connect("mongodb://mongodb0.example.com:27017"))# Connected to MongoDB!Compatibility with PyrightPyright has a small compatibility issue, you might get the following error:error: "my_function" is marked as overload, but it includes an implementation
The body of a function overload should be "..."This can be fixed without disabling type checking on calls and function bodies by adding# type: ignorenext to the overloaded function signatures. Here is a small example:fromdatetimeimportdatefromovertakeimportovertakefromtyping_extensionsimportoverload@overloaddefdays_this_year(current_date:date)->int:# type: ignoredelta=current_date-date(2023,1,1)returndelta.days@overloaddefdays_this_year(current_date:str)->int:# type: ignorereturndays_this_year(date.fromisoformat(current_date))@overtakedefdays_this_year(current_date):...To solve this limitation in the long run (Mypy might one day raise the same type of errors),
a draft PEP has been made. Don't hesitate to give feedback in the discussion!The PEP to offically support multiple dispatchDiscourse discussion