Python, a high-level dynamically typed programming language, has been lauded for its readability, simplicity, and versatility. However, it’s dynamic type system, which infers and assigns data types at run time rather than compile time, can sometimes be a double-edged sword. While it simplifies coding by eliminating the need for explicit type declarations, it can also lead to potential runtime type errors and performance bottlenecks. This is where Mypy, a static type checker for Python, comes into the picture.
What is Static Typing?
Static typing is a feature of programming languages like C++, Java, and Fortran, where type checking is performed at compile time. This approach contrasts with Python’s dynamic typing, whereby types are determined at runtime. The advantage of static typing is that it can prevent type errors before the program is run, enhancing code reliability. However, static typing requires explicit type declarations, which can make the code more verbose.
Mypy: Bridging the Gap
Mypy is an optional static type checker for Python, co-developed by Python’s creator, Guido van Rossum. Mypy allows Python programmers to enjoy the benefits of both dynamic and static typing. By providing optional static typing, Mypy helps in making Python code more correct, readable, and maintainable. It’s worth noting that while Mypy is the most widely used static type checker in the Python community, alternatives likePyre are also available.
Note: While static typing offers many benefits, it is often considered more challenging to learn and implement, especially for Python programmers used to dynamic typing.
Installation and Usage of Mypy
How to Install Mypy
Mypy can be installed using Python’s package manager, Pip. The command to install Mypy is as follows:
python -m pip install mypy
Note: If you’re using a different package manager or a Graphical User Interface (GUI) likeAnaconda, modify the installation command accordingly.
Basic Mypy Usage
Once you’ve installed Mypy, you can use it to check Python source files in your current directory:
mypy *.py
If no type errors are found in the code, Mypy will return a success message:
Success: no issues found in N source files
If you don’t have any Python files handy, you can create a simple one and check it with Mypy:
cd /tmp echo "print('Hello, world.')" > test1.py mypy *.py
This should return:
Success: no issues found in 1 source file
However, since the example doesn’t include any type annotations, Mypy doesn’t provide any useful static type information. The next section will show you how to add type annotations to your Python code.
Error Detection with Mypy
Mypy can help you catch errors in your code earlier in the development cycle. For instance, Python 3 requires parentheses around print
statements, unlike Python 2. If you’re updating a Python 2 program to Python 3, Mypy can help you identify syntax errors like missing parentheses.
Let’s create a Python file with a print
statement that lacks parentheses and run Mypy:
echo "print 'Hello, world.'" > test2.py mypy *.py
Mypy should return the following error:
error: Missing parentheses in call to 'print'. Did you mean print('Hello, world.')? Found 1 error in 1 file (errors prevented further checking)
This shows that Mypy can help identify every print
statement that requires parentheses in just one run.
Static Typing with Type Annotations
With Mypy, you can add type annotations to your Python functions to help detect errors related to incorrect function return types. Here’s an example:
def legal_name(first: str, last:str) -> str: return 'My legal name is:' + first + ' '+ last legal_name('Jane', 5)
If you run mypy test3.py
, you’ll get the following error message:
test3.py:4: error: Argument 2 to "legal_name" has incompatible type "int"; expected "str" Found 1 error in 1 file (checked 1 source file)
The function legal_name()
expects two string arguments and returns a string. Mypy detects that the second argument in the function call is an integer, not a string as specified by the type annotation. Without the type annotations, Mypy wouldn’t detect any issues with an integer argument.
Note: You can use Mypy’s
--disallow-untyped-defs
command-line option to enforce static typing on all function definitions. However, this may be too strict if your Python project interacts with third-party libraries that don’t use type annotations.
While this guide focuses on function signatures, Mypy recognizes type annotations on all objects in a Python program. When starting with Mypy, it’s advisable to focus on annotating your function definitions. Over time, you can consider adding type annotations to other variables in your code.
Type Aliases and Definitions
One of the powerful features of type annotations in Mypy is the ability to create domain-specific type definitions. These go beyond the built-in types and can make your code more expressive. For instance, consider this function:
def retrieve(url): """Retrieve the content found at url."""
We can add a type annotation to make the url
argument’s intent clearer:
URL = str def retrieve(url: URL) -> str: """Retrieve the content found at url."""
Here, URL
is atype alias that clarifies the url
argument’s type more than a simple str
does. The url
argument must be a valid URL that conforms to aspecific documented syntax.
Type aliases can also be used to abbreviate complex types. For instance, consider this function:
def compose(first: list[dict[str, float]], second: list[dict[str, float]]) -> list[dict[str, float]]
This can be rewritten more expressively using a type alias:
MyType = list[dict[str, float]] def compose(first: MyType, second: MyType) -> MyType:
Python also provides a powerful alternative to type aliases: type definitions. These can ensure that your arguments conform to specific syntax requirements. Here’s how to use a type definition to enforce correct URL syntax:
from typing import NewType URL = NewType("URL", str) def retrieve(url: URL) -> str:
With this type definition, Mypy will reject calls like retrieve("not a true URL")
and accept retrieve(URL("https://www.shape.host"))
. This allows you to enforce syntax checks at compile time rather than runtime.
Using Directives
Mypy’s directives can be used to adjust its output. Let’s create a Python file and run Mypy to see how directives work:
- Create a file named
test2.py
with the following content:
def f1(error: str) -> int: """While this does nothing particularly useful, the syntax is typical of Python code often found in the wild.""" if len(error) > 4: return 3 if len(error) > 10: return 1 + error return 99 print(f"The return value is {f1('abc')}.") print(f"The return value is {f1('abcef')}.")
- Run Mypy on
test2.py
with the--disallow-untyped-defs
option:
mypy --disallow-untyped-defs test2.py
Mypy recognizes the function definition in test2.py
as fully annotated, but it still reports an error:
test2.py:8: error: Unsupported operand types for + ("int" and "str")
- Rerun Mypy to display error codes:
mypy --show-error-codes --disallow-untyped-defs test2.py
The error message now includes the error code:
test2.py:8: error: Unsupported operand types for + ("int" and "str") [operator]
- Update the
return
statement in line 8 to include a directive:
return1 + error: # type: ignore[operator]
Rerunning Mypy now returns a success message, and you can continue working on other parts of your code. The # type: ignore[operator]
directive marks the issue to be fixed later. This strategy can be used more broadly when annotating types in a large body of source code: tackle one error type at a time and use directives to temporarily ignore other problems.
Configuring Mypy
You can configure Mypy using a configuration file named mypy.ini
. Here’s how to create a mypy.ini
file:
- Create a new file named
mypy.ini
in your project directory. - Add the following content to the file:
disallow_untyped_defs = true
Now, any mypy
command run in that directory will behave as though it was run with the --disallow-untyped-defs
argument. You can also store Mypy configurations in a .toml
file.
Your target configuration should include at least the following configurations:
disallow_untyped_defs = true no_implicit_optional = true show_error_codes = true strict_equality = true warn_redundant_casts = true warn_unused_ignores = true
This configuration aligns with the incremental approach to refactoring non-typed Python code recommended in this guide. It brings most of Mypy’s benefits to your project without involving its more complex aspects.
Wrapping Up
While type annotation is a significant change to Python coding, it promises substantial benefits. Mypy’s defaults, like not requiring annotation for the self
parameter in method definitions, are worth learning. For instance, a common constructor is:
def __init__(self, *args: str, **kwargs: str) -> None
Even with disallow_untyped_defs
set, Mypy correctly handles self
without requiring explicit annotation.
By following the techniques outlined in this guide, you can gradually migrate your Python projects to use type annotations, improving their correctness, readability, and maintainability.
As a final note, we want to highlight Shape.host, a reliable cloud hosting service provider, known for its Linux SSD VPS, that offers high-performance, scalable, and secure hosting solutions. Whether you’re a seasoned developer or a business owner, Shape.host can empower you with efficient hosting solutions to meet your unique needs.