Do union types actually exist in python?
Asked Answered
G

7

70

Since python is dynamically typed, of course we can do something like this:

def f(x):
    return 2 if x else "s"

But is this the way python was actually intended to be used? Or in other words, do union types exist in the sense they do in Racket for example? Or do we only use them like this:

def f(x):
    if x:
        return "s"

where the only "union" we need is with None?

Gourde answered 9/8, 2016 at 15:1 Comment(1)
To clarify, you mean the union types from Typed Racket? Python has nothing like those.Lot
H
78

Union typing is only needed when you have a statically typed language, as you need to declare that an object can return one of multiple types (in your case an int or str, or in the other example str or NoneType).

Python deals in objects only, so there is never a need to even consider 'union types'. Python functions return what they return, if the programmer wants to return different types for different results then that's their choice. The choice is then an architecture choice, and makes no difference to the Python interpreter (so there is nothing to 'benchmark' here).

Python 3.5 does introduce a standard for creating optional type hints, and that standard includes Union[...] and Optional[...] annotations. Type hinting adds optional static type checking outside of the runtime, the same way types in TypeScript are not part of the JavaScript runtime.

Hide answered 9/8, 2016 at 15:6 Comment(9)
Thank you! I understand that python is not statically typed. But I wanted to know if, in practice, it would ever be necessary to have a function that returns multiple types based on the parameter, or whether there'd ALWAYS be a way around it in python?Gourde
@Lana: that's way, way too broad. But take a look at pickle.loads() or json.loads(). These return arbitrary objects, based on what data is being loaded.Hide
@Lana: and again, it's a software architecture choice as to what a function returns. It's good practice to be consistent and limit what is returned, but 'a way around it' is just using good software engineering practices. If your function can return True, False, None or an integer for example, you need to rethink your function design.Hide
"there is never a need to even consider 'union types'". How does this align with the decision to introduce typing.Union?Vocalise
@joel: Python type hinting is static typing, added to Python. typing.Union is not a runtime type.Hide
It's 2022 and with Python 3.10+, we can use | for type Union operator. docs.python.org/3.10/whatsnew/…Iridic
Opinionated side note: Most things we "don't need", like Union Typing, often suggest there's a fundamental component in the lifecycle of programming that we are either under-utilizing, or overlooking. For myself, most of these I find include underlying libC, or unit testing.Lupelupee
@MartijnPieters: Runtime type-checkers in Python exist. I know these things, because I author one of them. typing.Union absolutely is a runtime thing that can be understood, introspected, and reasoned about at runtime. Indeed, runtime type-checkers offer profound advantages over static type-checkers: namely, they lie (i.e., emit false positives and negatives) alot less. Runtime type-checkers eliminate the need for # type: ignore spam while increasing confidence and trust in type-checking. These are good things.Trossachs
@CecilCurry: of course they do, but that doesn't mean that the Python runtime needs a union type like Racket's. The type hints are designed for static type checkers.Hide
A
39

the type itself does not exist because Python is just a dynamically typed language, however, in newer Python versions, Union Type is an option for Type Hinting,

from typing import Union,TypeVar

T = TypeVar('T')
def f(x: T) -> Union[str, None]:
    if x:
        return "x"

you can use that to annotate your code, thus enabling IDE/Editor level syntax checking.

Adenoidectomy answered 19/4, 2018 at 4:4 Comment(4)
can u explain T = TypeVar('T')Pandanus
@AlenPaulVarghese just read the manual: docs.python.org/3/library/typing.html#typing.TypeVarAdenoidectomy
@AlenPaulVarghese T = TypeVar('T') generates a named generic. The method provided in this answer will accept anything as input, and will return a string "x" if what was provided isn't None. Using a named generic here was entirely unnecessary, but I do suggest looking into them as they allow the creation of template functions which are incredibly useful.Valerievalerio
In addition to Syntax validation, as someone who is doing unit test development these are VERY useful for pre-release!Lupelupee
S
36

Note: As others mentioned, Python type hinting (by default) doesn't have any impact on runtime behavior, and it's used in static analysis and the like.

From Python 3.10 onwards, you can use | separator for union types. Taking the example from What's New In Python 3.10:

def square(number: int | float) -> int | float:
    return number ** 2

# Instead of 
def square(number: Union[int, float]) -> Union[int, float]:
    return number ** 2

Also, if you are using Python 3.7+, you can have the feature by using the __future__ package, with some limitations, however:

from __future__ import annotations

# Works in Python 3.7+
def square(number: int | float) -> int | float:
    return number ** 2

# Works only in Python 3.10+
isinstance(3.10, int | float)
numeric = int | float

For more information, see Union Types documentation and PEP 604.

Stedt answered 9/8, 2021 at 11:52 Comment(2)
Should union interpreted in a set theoretical manner? I.e. if an object can belong to both class A and class B, does A | B means that it can be both?Ibiza
@adosar, yes, it is "or" rather than "xor", and not only in Python, e.g. it works in PHP the same way.Stedt
T
13

Here are a couple of options to deal with use-cases where you need a tagged union/sum type in Python:

  • Enum + Tuples

    from enum import Enum
    Token = Enum('Token', ['Number', 'Operator', 'Identifier', 'Space', 'Expression'])
    
    (Token.Number, 42)                            # int
    (Token.Operator, '+')                         # str
    (Token.Identifier, 'foo')                     # str
    (Token.Space, )                               # void
    (Token.Expression, ('lambda', 'x', 'x+x'))    # tuple[str]
    

    A slight variation on this uses a dedicated SumType class instead of a tuple:

    from dataclasses import dataclass
    from typing import Any
    
    @dataclass
    class SumType:
        enum: Enum
        data: Any
    
    SumType(Token.Number, 42)
    
  • isinstance

    if isinstance(data, int):
        ...
    if isinstance(data, str):
        ...
    

    Or in combination with the "enum" idea from above:

    token = SumType(Token.Number, 42)
    
    if token.enum == Token.Number:
        ...
    
  • sumtypes module

These approaches all have their various drawbacks, of course.

Trueblue answered 24/9, 2018 at 1:59 Comment(0)
A
5

One use case not addressed by previous answers is building a union type from pre-existing types, and having isinstance() consider that any instance of the pre-existing types are instances of the union type.

This is supported in Python through Abstract Base Classes. For example:

>>> import abc
>>> class IntOrString(abc.ABC): pass
... 
>>> IntOrString.register(int)
<class 'int'>
>>> IntOrString.register(str)
<class 'str'>

Now int and str can be seen as subclasses of IntOrString:

>>> issubclass(int, IntOrString)
True
>>> isinstance(42, IntOrString)
True
>>> isinstance("answer", IntOrString)
True
Amorita answered 21/12, 2020 at 10:40 Comment(0)
O
1

Adding to @MartijnPieters answer:

But is the way python was actually intended to be used?

Returning different type depending on the param is never a good practice in any language. This makes testing, maintaining and extending the code really difficult and IMHO is an anti-pattern (but of course sometimes necessary evil). The results should at least be related via having common interface.

The only reason union was introduced to C was due to performance gain. But in Python you don't have this performance gain due to dynamic nature of the language (as Martijn noticed). Actually introducing union would lower performance since the size of union is always the size of the biggest member. Thus Python will never have C-like union.

Oni answered 9/8, 2016 at 15:10 Comment(8)
Thank you! That's exactly what I want to know though. When is using unions in python a necessary evil? And when we talk about "unions" do we talk about a union with none? (when I noticed a lot in python) or unions between different types? I was wondering if there is any example code that shows that.Gourde
Note that I don't think the OP is talking about the C union. I'm more thinking they have a Java or C# type system in mind.Hide
@Gourde As Martijn noticed json.loads() is an example of a necessary evil. "Unions" with None is a general practice but IMO should be avoided as well. Especially in bigger projects you just can't stop reading these NoneType object has no attribute xxx logs. My personal opinion: one function == one return type.Oni
@MartijnPieters I have no idea how unions work in other languages. Sorry, I can only refer to C unions.Oni
Thank you guys! json.loads() was exactly what I'm looking for. I needed to see examples of using multiple types and it seems streaming is one example. Are there any other situations? Or is there any where I can look to find more examples like these?Gourde
@Lana: again, that's way too broad. There are countless examples, I gave you two obvious ones. Go read the standard library documentation, you'll find plenty more.Hide
@Lana: It's not that json.loads() is exactly what you're looking for. It's more that json.loads() returns arbitrary types, including types that you, as a programmer, have not even thought of yet. Therefore, you cannot even in principle define a union type to cover everything that it can return.Lot
See tagged unions/sum types. These are much different in application from "C unions". Powerful statically typed languages like Haskell and Rust make extensive use of these.Trueblue
M
1

Updating the answer with as per Python 3.10. We can create a Union Type in python by separating the object types by '|'.

Example:

def method(mobject: int | str) -> int | str | None:
   pass
Millerite answered 28/4, 2023 at 18:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.