57

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?

Hari
  • 1,561
  • 4
  • 17
  • 26
Lana
  • 1,124
  • 1
  • 8
  • 17
  • 2
    To clarify, you mean the [union types from *Typed* Racket](https://docs.racket-lang.org/ts-guide/types.html#%28part._.Union_.Types%29)? Python has nothing like those. – John Y Aug 09 '16 at 23:19

7 Answers7

70

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 2
    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? – Lana Aug 09 '16 at 15:24
  • @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. – Martijn Pieters Aug 09 '16 at 15:25
  • 6
    @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. – Martijn Pieters Aug 09 '16 at 15:27
  • "there is never a need to even consider 'union types'". How does this align with the decision to introduce `typing.Union`? – joel Mar 05 '21 at 15:49
  • 2
    @joel: Python type hinting **is static typing**, added to Python. `typing.Union` is not a runtime type. – Martijn Pieters Mar 05 '21 at 15:55
  • 3
    It's 2022 and with Python 3.10+, we can use `|` for type Union operator. https://docs.python.org/3.10/whatsnew/3.10.html#pep-604-new-type-union-operator – Ziwon Jan 07 '22 at 16:00
  • 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. – Elysiumplain Mar 08 '22 at 22:24
  • 3
    @MartijnPieters: **Runtime type-checkers in Python exist.** I know these things, because [I author one of them](https://github.com/beartype/beartype). `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. – Cecil Curry Oct 28 '22 at 06:20
  • @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. – Martijn Pieters Nov 25 '22 at 14:38
36

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.

Sajuuk
  • 2,667
  • 3
  • 22
  • 34
  • 2
    can u explain T = TypeVar('T') – Alen Paul Varghese Oct 13 '20 at 06:22
  • @AlenPaulVarghese just read the manual: https://docs.python.org/3/library/typing.html#typing.TypeVar – Sajuuk Oct 15 '20 at 02:19
  • 6
    @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. – WebWanderer Nov 02 '20 at 20:22
  • In addition to Syntax validation, as someone who is doing unit test development these are VERY useful for pre-release! – Elysiumplain Mar 08 '22 at 22:28
28

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.

MAChitgarha
  • 3,728
  • 2
  • 33
  • 40
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.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
4

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
Yann Dirson
  • 109
  • 7
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.

freakish
  • 54,167
  • 9
  • 132
  • 169
  • 1
    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. – Lana Aug 09 '16 at 15:21
  • 2
    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. – Martijn Pieters Aug 09 '16 at 15:28
  • 2
    @Lana 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. – freakish Aug 09 '16 at 15:30
  • @MartijnPieters I have no idea how unions work in other languages. Sorry, I can only refer to C unions. – freakish Aug 09 '16 at 15:33
  • 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? – Lana Aug 09 '16 at 15:33
  • @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. – Martijn Pieters Aug 09 '16 at 15:48
  • 1
    @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. – John Y Aug 09 '16 at 23:28
  • 8
    See [tagged unions/sum types](https://en.wikipedia.org/wiki/Tagged_union#Examples). These are much different in application from "C unions". Powerful statically typed languages like Haskell and Rust make extensive use of these. – Mateen Ulhaq Sep 24 '18 at 01:39
0

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