Precision Python: Leveraging mypy and Pylint for Type Safety
Python's dynamic typing is flexible but error-prone. Static type checking with mypy and Pylint enhances code reliability and maintainability.
Join the DZone community and get the full member experience.
Join For FreeHandling Typing in Python by Default
Python's dynamic typing is a simple concept by default. It does not enforce explicit type declarations, allowing variables to change types at runtime. The variable type is determined based on its value at any given time, making it easy to understand and work with.
x = 10 # x is an integer
x = "hello" # now x is a string
Since type checking occurs at runtime, type errors can occur during execution if the code tries to perform an operation not supported by the variable's type.
x = 10
x = x + "hello" # TypeError: unsupported operand type(s) for +: 'int' and 'str'
Dynamic typing promotes ease of use and rapid development but requires careful handling to avoid runtime type errors.
Why Type Checking Is Important
Typechecking in Python provides numerous benefits, including early error detection, improved readability and maintainability, enhanced tooling support, better documentation, bug prevention, and overall code quality improvement. Even though Python is dynamically typed, type hints and static type checking can significantly enhance the development process and help maintain large codebases more effectively.
Available Tools for Type Checking
Now that we know the importance of type checking, there are a fair few tools that can help with it, such as mypy, Pyright, Pylint, Pyre, Typeguard, and Pydantic. In this article, we will compare two of the most widely adopted type-checking tools: mypy and Pylint.
Code With Intentional Typing Error
In order to showcase the importance of mypy and Pylint, we need a good example of code that can be applied with both of these tools. So, we have a piece of code with intentional typing errors to help us with that.
from typing import List, Dict, Optional
def fetch_user(user_id: int) -> Optional[Dict[str, str]]:
users = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"}
}
return users.get(user_id)
def add(a: int, b: int) -> int:
return a + b
def process_data(data: List[int]) -> None:
for item in data:
print(item)
def main() -> None:
user = fetch_user(1)
if user:
print(user["name"])
result = add(10, "20") # Intentional error: second argument should be int
process_data([1, 2, 3])
process_data("not a list") # Intentional error: should be a list of integers
if __name__ == "__main__":
main()
mypy vs. Pylint
Now, let's run the above code with intentional typing errors using mypy and Pylint and check the outputs.
mypy
Introduced to enhance Python's dynamic typing, mypy helps developers catch type-related errors at compile time rather than at runtime. By analyzing type annotations in the code, mypy verifies that functions and variables are used according to their specified types. This leads to early bug detection, improved code readability, and easier maintenance.
Installation
pip install mypy
Execution
mypy intentionaltypingerror.py
Output
mypy intentionaltypingerror.py
intentionaltypingerror.py:23: error: Argument 2 to "add" has incompatible type "str"; expected "int" [arg-type]
intentionaltypingerror.py:26: error: Argument 1 to "process_data" has incompatible type "str"; expected "list[int]" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
As mypy focuses on type checking, it detected that the second argument to add should be an integer, but a string was provided. It also caught that process_data
was called with a string instead of a list of integers.
Pylint
Similar to mypy, Pylint also performs static code analysis on Python code to enforce coding standards and check for type-related errors. By examining the source code without executing it, Pylint detects potential issues and provides suggestions for enhancing code quality and adhering to best practices.
Installation
pip install pylint
Execution
pylint intentionaltypingerror.py
Output
Pylint intentionaltypingerror.py
************* Module intentionaltypingerror
intentionaltypingerror.py:22:0: C0303: Trailing whitespace (trailing-whitespace)
intentionaltypingerror.py:24:0: C0303: Trailing whitespace (trailing-whitespace)
intentionaltypingerror.py:1:0: C0114: Missing module docstring (missing-module-docstring)
intentionaltypingerror.py:4:0: C0116: Missing function or method docstring (missing-function-docstring)
intentionaltypingerror.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
intentionaltypingerror.py:14:0: C0116: Missing function or method docstring (missing-function-docstring)
intentionaltypingerror.py:18:0: C0116: Missing function or method docstring (missing-function-docstring)
intentionaltypingerror.py:23:4: W0612: Unused variable 'result' (unused-variable)
-----------------------------------
Your code has been rated at 5.56/10
As you can see, Pylint provides a broader range of checks, including code style and potential errors. It found that a string was passed where a list was expected and that a string was used where an integer was expected.
Corrected Code and Conclusion
Now, let's correct the code and rerun mypy and Pylint on it to make sure the errors are taken care of. We should also reemphasize the importance of checking for typing errors at compile time instead of runtime.
Corrected Code
"""
Example module for demonstrating type checking and linting with mypy and pylint.
"""
from typing import List, Dict, Optional
def fetch_user(user_id: int) -> Optional[Dict[str, str]]:
"""
Fetches a user by ID.
Args:
user_id (int): The ID of the user to fetch.
Returns:
Optional[Dict[str, str]]: The user information if found, None otherwise.
"""
users = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"}
}
return users.get(user_id)
def add(a: int, b: int) -> int:
"""
Adds two integers.
Args:
a (int): The first integer.
b (int): The second integer.
Returns:
int: The sum of the two integers.
"""
return a + b
def process_data(data: List[int]) -> None:
"""
Processes a list of integers.
Args:
data (List[int]): A list of integers.
"""
for item in data:
print(item)
def main() -> None:
"""
The main function that runs the example code.
"""
user = fetch_user(1)
if user:
print(user["name"])
result = add(10, 20) # Corrected: second argument is now an int
print(result) # Use the result variable
process_data([1, 2, 3])
# process_data("not a list") # Removed: incorrect call with a string
if __name__ == "__main__":
main()
Now, let's see the output of mypy and Pylint on the corrected code. As you noticed, we have corrected both intentional errors.
Output mypy and Pylint
mypy intentionaltypingerror.py
Success: no issues found in 1 source file
pylint intentionaltypingerror.py
-------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 5.56/10, +4.44)
Comparison
Features |
mypy |
Pylint |
Primary Purpose |
Static Type Checking |
Static Code Analysis and Linting |
Error Detection |
Type-related Error |
Code Style, Potential errors, Type Hints |
Tooling Support |
Integration with IDEs, CI/CD Pipelines |
Integration with IDEs, CI/CD Pipelines |
Ease of Use |
Medium(requires type annotations) |
High(focuses on code style) |
Popularity |
High among developers using type hints |
Very High for general code quality |
Conclusion
In the realm of Python development, mastering the balance between dynamic and static typing can significantly enhance code quality and maintainability. While Python’s dynamic typing promotes flexibility and rapid development, it can lead to runtime errors if not handled carefully. Tools like mypy and Pylint play a crucial role in mitigating these risks. Mypy focuses on static type checking, catching type-related errors early, and improving code reliability. Pylint, on the other hand, offers a broader scope, enforcing coding standards and identifying potential issues beyond typing errors. By incorporating mypy and Pylint into the development workflow, one can harness the strengths of both tools to create a more robust, readable, and maintainable codebase.
Opinions expressed by DZone contributors are their own.
Comments