Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

More Precise Types

In this lesson, you’ll learn about more precise types in Python 3.8. Python’s typing system is quite mature at this point. However, in Python 3.8, some new features have been added to typing to allow more precise typing:

  • Literal types
  • Typed dictionaries
  • Final objects
  • Protocols

Python supports optional type hints, typically as annotations on your code:

Python
>>> def double(number: float) -> float:
...     return 2 * number

In this example, you say that number should be a float and double() should return a float as well. However, Python treats these annotations as hints. They are not enforced at runtime:

Python
>>> double(3.14)
6.28

>>> double("I'm not a float")
"I'm not a floatI'm not a float"

double() happily accepts "I'm not a float" as an argument, even though that’s not a float.

Type hints allow static type checkers to do type checking of your Python code, without actually running your scripts. This is reminiscent of compilers catching type errors in other languages like Java, and Rust. Additionally, type hints act as documentation of your code, making it easier to read, as well as improving auto-complete in your IDE.

Try out Mypy on the code example from before. Create a new file named float_check.py:

Python
# float_check.py

def double(number: float) -> float:
    return 2 * number

double(3.14)

double("I'm not a float")

Now run Mypy on this code:

Shell
$ mypy float_check.py
float_check.py:8: error: Argument 1 to "double" has incompatible
                        type "str"; expected "float"
Found 1 error in 1 file (checked 1 source file)

Based on the type hints, Mypy is able to tell you that you are using the wrong type on line 8. Change the argument for the second call to double() to a float:

Python
# float_check.py

def double(number: float) -> float:
    return 2 * number

double(3.14)

double(2.4)

Now run Mypy on this code again:

Shell
$ mypy float_check.py
Success: no issues found in 1 source file

In some sense, Mypy is the reference implementation of a type checker for Python, and is being developed at Dropbox under the lead of Jukka Lehtasalo. Python’s creator, Guido van Rossum, is part of the Mypy team.

You can find more information about type hints in Python in the original PEP 484, as well as in Python Type Checking (Guide), and the video course Python Type Checking.

There are four new PEPs about type checking that have been accepted and included in Python 3.8. You’ll see short examples from each of these.

PEP 586 introduced the Literal type. Literal is a bit special in that it represents one or several specific values. One use case of Literal is to be able to precisely add types, when string arguments are used to describe specific behavior. Consider the following example:

Python
# draw_line.py

def draw_line(direction: str) -> None:
    if direction == "horizontal":
        ...  # Draw horizontal line

    elif direction == "vertical":
        ...  # Draw vertical line

    else:
        raise ValueError(f"invalid direction {direction!r}")

draw_line("up")

The program will pass the static type checker, even though "up" is an invalid direction. The type checker only checks that "up" is a string. In this case, it would be more precise to say that direction must be either the literal string "horizontal" or the literal string "vertical". Using Literal, you can do exactly that:

Python
# draw_line.py

from typing import Literal

def draw_line(direction: Literal["horizontal", "vertical"]) -> None:
    if direction == "horizontal":
        ...  # Draw horizontal line

    elif direction == "vertical":
        ...  # Draw vertical line

    else:
        raise ValueError(f"invalid direction {direction!r}")

draw_line("up")

By exposing the allowed values of direction to the type checker, you can now be warned about the error:

Shell
$ mypy draw_line.py
draw_line.py:15: error:
    Argument 1 to "draw_line" has incompatible type "Literal['up']";
    expected "Union[Literal['horizontal'], Literal['vertical']]"
Found 1 error in 1 file (checked 1 source file)

The basic syntax is Literal[<literal>]. For instance, Literal[38] represents the literal value 38. You can express one of several literal values using Union:

Python
Union[Literal["horizontal"], Literal["vertical"]]

Since this is a fairly common use case, you can (and probably should) use the simpler notation Literal["horizontal", "vertical"] instead. You already used the latter when adding types to draw_line().

If you look carefully at the output from Mypy above, you can see that it translated the simpler notation to the Union notation internally.

There are cases where the type of the return value of a function depends on the input arguments. One example is open(), which may return a text string or a byte array depending on the value of mode. This can be handled through overloading.

The following example shows the skeleton of a calculator that can return the answer either as regular numbers (38) or as roman numerals (XXXVIII):

Python
# calculator.py

from typing import Union

ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
                   (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
                   (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]

def _convert_to_roman_numeral(number: int) -> str:
    """Convert number to a roman numeral string"""
    result = list()
    for arabic, roman in ARABIC_TO_ROMAN:
        count, number = divmod(number, arabic)
        result.append(roman * count)
    return "".join(result)

def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
    """Add two numbers"""
    result = num_1 + num_2

    if to_roman:
        return _convert_to_roman_numeral(result)
    else:
        return result

The code has the correct type hints: the result of add() will be either str or int. However, often this code will be called with a literal True or False as the value of to_roman, in which case you would like the type checker to infer exactly whether str or int is returned. This can be done using Literal together with @overload:

Python
# calculator.py

from typing import Literal, overload, Union

ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
                   (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
                   (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]

def _convert_to_roman_numeral(number: int) -> str:
    """Convert number to a roman numeral string"""
    result = list()
    for arabic, roman in ARABIC_TO_ROMAN:
        count, number = divmod(number, arabic)
        result.append(roman * count)
    return "".join(result)

@overload
def add(num_1: int, num_2: int, to_roman: Literal[True]) -> str: ...
@overload
def add(num_1: int, num_2: int, to_roman: Literal[False]) -> int: ...

def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
    """Add two numbers"""
    result = num_1 + num_2

    if to_roman:
        return _convert_to_roman_numeral(result)
    else:
        return result

The added @overload signatures will help your type checker infer str or int depending on the literal values of to_roman. Note that the ellipses (...) are a literal part of the code. They stand in for the function body in the overloaded signatures.

As a complement to Literal, PEP 591 introduces Final. This qualifier specifies that a variable or attribute should not be reassigned, redefined, or overridden. The following is a typing error:

Python
# final_id.py

from typing import Final

ID: Final = 1

...

ID += 1

Mypy will highlight the line ID += 1, and note that you Cannot assign to final name "ID". This gives you a way to ensure that constants in your code never change their value.

Additionally, there is also a @final decorator that can be applied to classes and methods. Classes decorated with @final can’t be subclassed, while @final methods can’t be overridden by subclasses:

Python
# final_class.py

from typing import final

@final
class Base:
    ...

class Sub(Base):
    ...

Mypy will flag this example with the error message Cannot inherit from final class "Base". To learn more about Final and @final, see PEP 591.

The third PEP allowing for more specific type hints is PEP 589, which introduces TypedDict. This can be used to specify types for keys and values in a dictionary using a notation that is similar to the typed NamedTuple.

Traditionally, dictionaries have been annotated using Dict. The issue is that this only allowed one type for the keys and one type for the values, often leading to annotations like Dict[str, Any]. As an example, consider a dictionary that registers information about Python versions:

Python
py38 = {"version": "3.8", "release_year": 2019}

The value corresponding to version is a string, while release_year is an integer. This can’t be precisely represented using Dict. With the new TypedDict, you can do the following:

Python
# typed_dict.py

from typing import TypedDict

class PythonVersion(TypedDict):
    version: str
    release_year: int

py38 = PythonVersion(version="3.8", release_year=2019)

The type checker will then be able to infer that py38["version"] has type str, while py38["release_year"] is an int. At runtime, a TypedDict is a regular dict, and type hints are ignored as usual:

Python
>>> from typed_dict import *
>>> py38
{'version': '3.8', 'release_year': 2019}
>>> type(py38)
<class 'dict'>

Mypy will let you know if any of your values has the wrong type, or if you use a key that has not been declared. See PEP 589 for more examples.

Mypy has supported Protocols for a while already. However, the official acceptance only happened in May 2019.

Protocols are a way of formalizing Python’s support for duck typing:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck. (Source)

Duck typing allows you to, for example, read .name on any object that has a .name attribute, without really caring about the type of the object. It may seem counter-intuitive for the typing system to support this. Through structural subtyping, it’s still possible to make sense of duck typing.

You can for instance define a protocol called Named that can identify all objects with a .name attribute:

Python
# protocol.py

from typing import Protocol

class Named(Protocol):
    name: str

def greet(obj: Named) -> None:
    print(f"Hi {obj.name}")

class Dog:
    ...

x = Dog()

greet(x)

After creating the Named protocol, and defining the function greet() that takes an argument of obj, the type hint is specifying that obj follows the Named protocol. Run the code through Mypy to see what it finds:

Shell
$ mypy protocol.py
protocol.py:16: error: Argument 1 to "greet" has incompatible type "Dog"; expected "Named"
Found 1 error in 1 file (checked 1 source file)

The Dog class did not have an attribute of .name, and therefore did not meet the check for the Named protocol. Add a .name attribute to the class, with a default string:

Python
# protocol.py

from typing import Protocol

class Named(Protocol):
    name: str

def greet(obj: Named) -> None:
    print(f"Hi {obj.name}")

class Dog:
    name = 'Good Dog'

x = Dog()

greet(x)

Run protocol.py through Mypy again:

Shell
$ mypy protocol.py
Success: no issues found in 1 source file

As you have confirmed, greet() takes any object, as long as it defines a .name attribute. See PEP 544 and the Mypy documentation for more information about protocols.

00:00 In this video, I’ll show you Python 3.8’s more precise types. There are four enhancements that Python 3.8 has made to typing. The first one I’ll show you is literal types, then typed dictionaries, final objects, and protocols.

00:16 I’ll provide examples and resources on each one. If you’re not familiar with type checking and type hints, let me give you a quick summary and some resources where you can learn more on your own. Python supports optional type hints, typically as annotations on your code. It looks like this.

00:34 For this example, you’re saying that number should be a float, in that the function double() should return a float as well.

00:41 It should be noted that Python treats these annotations as hints. They’re not enforced at runtime. Let me show you what I mean. Into the REPL, define double().

00:52 That takes a number. In this case, you’re adding the type hint of number as a float, and that this function returns a float also.

01:00 So, those are the annotations. And it returns 2 * the argument number. If you call double() and give it an actual float, it will return a float.

01:10 But remember, these are just hints. Python doesn’t have any enforcement at runtime. You could call double() and give it a str (string) instead of a float, which—using that operator—replicates the str. Because of this lack of enforcement of types, that’s where you may want to look at a static type checker.

01:29 The type hints that you add to your code allow a static type checker to do actual type checking of your code. In a lot of ways, it’s similar to how a compiler catches type errors in other languages, like Java or Rust. In this video, you’ll use a type checker called Mypy as the static type checker.

01:47 It would be installed by using pip install mypy. Since I just upgraded to version 3.8, I’ll install it also. First off, I’m going to exit the REPL.

01:58 So it would be python3 -m and then I’m using pip to install mypy.

02:06 Okay! Now it’s installed. To run mypy on your code, the code would need to be in a file, not just running in a REPL. Create a new file just called float_check.py.

02:17 It’s going to have the same exact code from before—you can copy the code in if you’d like—but also calling double() twice here with the two styles. You’ve created float_check.py and saved it. Down here in the terminal to do type checking on this, you would type mypy instead of python, and then the name of the file.

02:39 And here, it sees that there is an error: Argument 1 to "double" has an incompatible type "str". So here on line 6, you gave it a str instead of a float. So, 1 error.

02:49 If this was changed to say 2.4—save, run mypy on it again—here it says Success: no issues found in 1 source file.

02:59 If you’d like to dive in much deeper and get more information about type hints in Python, check out PEP 484, and here on Real Python there’s a video course and an article that goes much deeper into type checking.

03:10 Links are below this video.

03:13 Now that you’ve been through a quick overview on type hints, let’s cover what’s new in Python 3.8. The first of these new precise types is called the Literal type. PEP 586 introduces it. Literal types indicate that a parameter or return value is constrained to one or more very specific literal values—literally, they can only be those values. Like in this example from the Python docs, def get_status(). Here, you’re saying port, that’s expecting an int (integer), and then it’s going to return a Literal of 'connected' or 'disconnected'.

03:45 Those are the only two available return values—literally, 'connected' or 'disconnected'. Let me have you look at Literal types with a little more code.

03:53 Create a new file and name it draw_line.py. You can find the code in the text below the video. Go ahead and just copy and paste the code in. The code is a skeleton example, setting up a function called draw_line() with its type hints. So here, draw_line() takes an argument of direction, which has a type hint indicating it requires a str, and the function—as an example—it’s currently returning None.

04:15 But what it’s doing here is it’s looking for that str to be either the direction "horizontal" or "vertical", else: it’s going to raise an error that it’s an invalid direction.

04:25 And after defining draw_line(), you’re calling it, giving it a direction of "up". Since direction can take a str, "up" isn’t going to raise an error, as far as running the type checking on it.

04:36 Go ahead and save draw_line and I’ll show you what I mean. I’m going to open up a terminal here,

04:41 and then run mypy on the code. So draw_line.py, and it says there’s no issues found in 1 source file. This is where Literal is going to help in this case.

04:53 You can actually specify with Literal that these are literally the only two text strings that you’re looking for: a "horizontal" or a "vertical" line.

05:01 So, how do you change the code to look like that? Up here on the top—I’m going to go ahead and minimize the terminal. Up at the top of the file, from typing you’re going to import this new type Literal.

05:11 And then when you get into draw_line(), you’re still taking direction here, but instead of it accepting a type of str, you’re going to say, “No, I want it to be a Literal. The Literal is going to take—in this particular case—a list of ["horizontal", "vertical"]. Again, it’s still just returning None, and the rest of the code is going to look pretty similar.

05:28 You’re not going to make any other changes here. It’s accepting a Literal now instead of just a str type. So go ahead and save. Now try to run mypy on that code one more time.

05:38 Now this time, you get a slightly different response from mypy. It says here on line 15, that you have an error: Argument 1 to "draw_line" has incompatible type "Literal['up']".

05:51 It expected either 'horizontal' or 'vertical'. You’re seeing a slightly different notation here, with Union and then [Literal['horizontal'], Literal['vertical']].

06:00 You can express one of several literal values using that Union statement, but what’s nice is there’s a simpler notation that you used above here that simply just accepts a list. So, it found this one error in your file.

06:13 So now mypy knows literally what to look for. Let me have you look at another example. There are cases where the type of a return value of a function depends on the input arguments. An example of that would be the function open(), which in that case could be returning a text string or it could be a byte array.

06:28 This would lead to a loose signature for what you’re returning from the function with a Union saying you could return either. But there is a feature called function overloading that can help you deal with this, and there’s a link to the Mypy docs that explain this in more depth.

06:43 This next example shows the skeleton of a calculator that can return an answer as regular numbers or Roman numerals. So, go ahead and close draw_line and create a new file, and call it calculator. Again, the code that you’re going to use is in the text below the video.

06:58 Go ahead and copy the code for calculator.py in.

07:04 From typing you’re importing Union,

07:06 and here’s a big list of tuples that are taking Arabic to Roman. And this function uses it, _convert_to_roman_numeral(). In that case, it’s taking a number of an int and returning a str.

07:19 The function that we’re most interested in is add(), and it takes two numbers, num_1 and num_2, which are integers, and then a third argument to_roman, which has a hint of bool and a default value of True. So by default, it’s going to return a Roman numeral. And this is where the confusion comes in as far as the return statement.

07:37 It shows that it’s a Union of either str or int. Down here, you have an if statement, which is taking that third argument and saying “Either convert it to a Roman numeral—which would return a str—or return the result, which would be a standard int from those two numbers.” Okay.

07:53 Don’t worry too much about the math and what’s going on inside there. The main thing here is to focus on this, that the code has the correct type hints and the result of add() will either be a str or an int. However, this code will be called with a literal True or False, as the value of to_roman.

08:11 In which case you’d like the type checker to infer exactly whether it’s going to be a str or an int that’s returned. This can be done using Literal with @overload. Let me show you what I mean.

08:22 You’re going to modify this slightly.

08:26 Overloading

08:30 the add() function,

08:34 and you can just copy that to start. And here you can say to_roman is a Literal

08:42 of True. So for this version, the return would be a str.

08:52 And you can copy that. In this version, when it’s False,

08:57 then it will be an int.

09:00 Go ahead and put the ellipsis (...) at the end of each of these @overload statements. The added @overload signatures will help the type checker infer str or int, depending on the literal values of to_roman. Note that the ellipses (...) are a literal part of the code.

09:14 They stand in for the function body in the overloaded signatures.

09:19 The next type is introduced in PEP 591, and it’s called Final. The Final qualifier specifies that a variable or attribute should not be reassigned, redefined, or overridden. Let me show you an example.

09:33 Create a new file and call it final_id.py, and you can just copy the code in. But basically you’re importing from the typing module the new Final type, and here you’re defining ID as a Final with a type hint, and that has a value as 1.

09:48 And then later on you’re reassigning ID by incrementing it by 1. If you were to run this through mypy, after saving,

09:58 it shows a typing error on line 7, noting that you Cannot assign to final name "ID". Again, ID is of type Final. You can’t make any other additional assignments to it.

10:11 So this is going to give you a way to ensure that constants such as this ID in your code never change their value.

10:19 Additionally, there’s a new @final decorator that can be applied to classes and methods. When @final decorates a class, it means that the class can’t be subclassed. @final methods can’t be overridden by subclasses.

10:35 Let me show you that in code also. Create a new file and call it final_class. You can copy and paste the code again. But here, again, importing final from typing, and then using the @final decorator.

10:51 So in this case, we’re saying that this is a final class, that class Base should not be able to be subclassed, but here later on, you’re going ahead and trying to do that.

11:00 What happens if you run that through the type checker? It says here that on line 7, you have an error. It Cannot inherit from final class "Base", creating that error and indicating that this class Base cannot be subclassed. And PEP 589 introduces the typed dictionary.

11:19 A TypedDict can be used to specify types for keys and values in a dictionary using a notation that’s similar to the typed NamedTuple. Up to now, dictionaries have been annotated just using Dict, for dictionary.

11:33 The problem with that is that a Dict only allows for one type for the keys and one type for the values, and that often leads to annotations like this—str and then Any—to try to get around the fact that there can only be one type for both keys and values, let me have you try out this new type with some code.

11:53 Create a new file and call it typed_dict.py. What if you wanted to create a dictionary that registers information about Python versions? So here, for Python 3.8, you would have two keys—one of "version" and "release_year"but the values would be different types, in this case, being a string and an integer. That’s where you run into Dict having to show str and Any, like the example in the slide. So with the new TypedDict, you can do the following. I’m just going to copy and paste the code in.

12:25 You would import from typing the TypedDict, and in this case you would then create a class and give it a name—in this case, you’re calling it PythonVersion and it’s of TypedDict. And then from here, you start adding your keys and then the types. Here you’re creating py38, and it’s equal to the type—in this case, of a PythonVersionand then here you’re adding the values in.

12:48 So the type checker will then be able to infer that version should have a type of str and release_year should have type of an int. Save.

12:57 If you were to open up a REPL and you were to import everything from that—like a module, from TypedDict import *—the object py38 is there, and you could look at the type of it. py38 is a Python dictionary, but for type checking purposes, it is this new type of TypedDict.

13:21 PEP 544 covers Protocols. Protocols are a way of formalizing Python support for duck typing. If you’re not familiar with duck typing, let me give you a heads up on it real quick. The name comes from the phrase “When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.” In the case of typing, it’s saying that the type or the class of an object is less important than the methods it defines. So here, instead of checking for the specific class or type, you can check the object for the presence of specific methods and/or attributes.

13:56 So how does this relate to protocols? Well, with protocols, you can define a protocol that can identify all the objects with a particular attribute. Let me give you an example.

14:08 Let me have you create an instance to define a protocol called Named, and it’s going to identify all objects that have a .name attribute.

14:16 So create a new file, and this one you’re going to call it protocol.py, and then you can copy the code in again. So here, again you’re importing from typing, Protocol and you’re creating a new class that’s called Named that has a type Protocol. And then inside there, that’s where you’re specifying the attributes. So in this case, it’s going to have the attribute .name, which has a type str. Here, greet() takes any object as long as it defines a .name attribute.

14:41 It’s time to test the protocol. Create a new class of Dogand that’s it. Then create an object x and make it up of class Dog.

14:51 And then you said “Okay, greet the dog,” by using greet(x). If you ran this through mypy, it would say here Argument 1 to "greet" has incompatible type "Dog". It expected "Named".

15:03 So it’s not of that protocol Named. What would have to change is your class of Dog would need a .name attribute. And let’s say the default is 'Good dog'. Run protocol.py through mypy one more time.

15:16 Now I have no issues, because it now has an attribute of .name. A protocol is going to look for specific attributes. In this particular case, the protocol you created requires that there is a .name.

15:30 In the next video, you get a chance to try out simpler debugging with f-strings.

Avatar image for Shehu Bello

Shehu Bello on March 26, 2020

Very Interesting features. However, with these features, python is looking like a statically typed languages such as C.

Avatar image for fofanovis

fofanovis on April 1, 2020

Note that the ellipses (…) are a literal part of the code. They stand in for the function body in the overloaded signatures.

Could you be so kind to elaborate on that?

Avatar image for fofanovis

fofanovis on April 1, 2020

Traditionally, dictionaries have been annotated using Dict. The issue is that this only allowed one type for the keys and one type for the values, often leading to annotations like Dict[str, Any].

The link you provided leads to the documentation where Dict is used in a function declaration and Dict declares both types and they are different.

def count_words(text: str) -> Dict[str, int]:
    ...
Avatar image for fofanovis

fofanovis on April 1, 2020

Oh, I guess I got it. The TypedDict is a case when we know all the keys preliminary. There’s no way to add as many key-value pairs as we want to a TypedDict. On the contrary, we can do that with a regular Dict. Actually, these types are very different, in the end. Not even saying about the typing differences.

Avatar image for Geir Arne Hjelle

Geir Arne Hjelle RP Team on April 1, 2020

Right! I guess you could classify most uses of dictionaries as one of two use cases:

  1. Dictionaries used as a kind of indexed lists, where you usually don’t need a clear idea up front about how many items you’ll have. For example:

    capitals = { "Afghanistan": "Kabul", "Albania": "Tirana", ... }

    For these cases, something like Dict[str, str] is a good description of the types used in the dictionary.

  2. Dictionaries used similarly to a database row or a named tuple. In this case, the structure of the dictionary is often more strict. For example:

    person = { "first name": "Guido", "birthyear": 1956, ... }

    For these cases, the values are often of different types, so that the only “simple” type that can be conveniently used is Dict[str, Any]. However, Any does not really provide any useful information. Instead, TypedDict can be used to properly capture the type of these dictionaries.

    I would guess that most actual uses of this second kind of dictionaries, have these dictionaries nested inside another data structure, like a list or case-1-dictionary (for example of persons).

Avatar image for Chris Bailey

Chris Bailey RP Team on April 1, 2020

Hi @fofanovis, Regarding the ellipsis ... in the two @overload signatures @overload def add(num_1: int, num_2: int, to_roman: Literal[True]) -> str: ... @overload def add(num_1: int, num_2: int, to_roman: Literal[False]) -> int: ... The overload is not redefining the function, but redefining the expected types to be returned. And the syntax allows you to not include the whole function definition in the overload statement, but to use the ellipsis as a placeholder for the rest of the function. This discussion on python/typing ‘stubs’ issues 109 covers some of it. I think Geir Arne would know a bit more, as he is the author of the original article.

Avatar image for varelaautumn

varelaautumn on Sept. 28, 2020

I really like that literal option. One of the most jarring things for me in learning Python was the amount of hardcoded strings used in code. In C# it was always stressed to me that this was the worst conceivable thing you could do because of how easy it is for typos to go unnoticed and cause errors.

I usually got around this in C# using enums (like color.RED, color.BLUE, etc., instead of “red”, “blue”). Is this an acceptable alternative? So like using your example, have an enum with direction.HORIZONTAL and direction.VERTICAL as the two members of the enum direction?

The protocol one sounds great as well, and I’ve been specifically wondering if something like that existed so I’m glad to hear that explained.

Though I’m still uncertain on how acceptable it is to use all this typing? I just never seem to see it in code I read. Would people be frustrated with my code if every function included type hints?

Become a Member to join the conversation.