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:
>>> 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:
>>> 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.
Note: There are several static type checkers available, including Pyright, Pytype, and Pyre. In this course, you’ll use Mypy. You can install Mypy from PyPI using pip:
$ python -m pip install mypy
Try out Mypy on the code example from before. Create a new file named float_check.py
:
# 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:
$ 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
:
# float_check.py
def double(number: float) -> float:
return 2 * number
double(3.14)
double(2.4)
Now run Mypy on this code again:
$ 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:
# 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:
# 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:
$ 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
:
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
):
# 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
:
# 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:
# 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:
# 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:
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:
# 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:
>>> 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:
# 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:
$ 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:
# 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:
$ 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: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
,
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 PythonVersion
—and 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 Dog
—and 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.
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?
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]:
...
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.
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:
-
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. -
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
).
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.
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.
Shehu Bello on March 26, 2020
Very Interesting features. However, with these features, python is looking like a statically typed languages such as C.