Three friends of the better code style – Python
Want to share your content on python-bloggers? click here.
Mypy, Pylint, and Black are three tools you should know in the world of Python coding.
Even if you’ve never used them before, these tools are what separates professionals from amateurs. And if you happen to be looking for code style guides for Python that are based on what is written in the official language documentation, you’ll be delighted to try them out in your own project.
You can use them in addition with other tips & tricks we use in Python language – check the Tools for Python scripting: Docopt, setup.py and ConfigParser for more recommendations.
What is Mypy?
Mypy is a static type checker for Python. The library behaves like a linter, and statically scans the code without running any chunk of it. It can resolve numerous common issues with the dynamic typing mechanism and improve our code clarity by checking given type annotations.
Disclaimer: Please note that Mypy is still a beta library.
How to use Mypy?
To install Mypy, make sure that you have Python in version 3.5 or higher.
To install Mypy, use PIP with the following command: pip3 install mypy
Note: all commands have been checked on the Linux distribution (Debian/Ubuntu).
Mypy commands scan the code and print any found errors to the terminal output.
Run Mypy to scan a single file: mypy my_code.py
or loop over all .py
files: find . -iname '*.py' | xargs mypy
Configuration and additional information
Mypy allows reading rules from files. Thanks to that, we can create a mypy.ini
file with Mypy configuration. To use the configuration file, place it in the root of the project.
To set up a global option, place rules under [mypy]
or set rules only for specific modules where Mypy is a global required flag and code.test
is a module [mypy-code.test]
. Furthermore, the best approach is to define the Python version in the config file, just to be clear on which version Mypy will work.
Sample configuration file:
[mypy] python_version = 3.9 warn_redundant_casts = True [mypy-code.test] warn_return_any = False ignore_missing_imports = True
Where:
warn_redundant_casts
— Mypy will log an error about casting an expression to its inferred type. It is useful as it preventscast()
function abuse, leaving only the places where cast would have any effect at all.warn_return_any
— registers an error if the valueAny
is returned from a method that returns another declared type. For tests, we do not need this check to avoid finding issues in the code that was added before the introduction of Mypy.ignore_missing_imports
— Mypy will either ignore module imports that are not included in module names or it will ignore imports of external libraries. It is the most common rule in case when we are working with multiple modules or third party libraries.
You can find the list of rules here: https://mypy.readthedocs.io/en/stable/config_file.html. Be careful, however, which rule you add to the configuration file because some of them can be placed only under the global module ([mypy]
). All global rules have additional information — “This option may only be set in the global section ([mypy]
).”
Ignore rules
Sometimes third party library imports or functions with multiple arguments appear in our code, and Mypy can treat them as errors. To avoid this, we can ignore whole files or specific rules.
To ignore all code, add : # type: ignore
at the beginning of the file, or ignore a single line by adding the # type: ignore
comment at the end of the line.
Generally, if we don’t want to ignore the whole file, a better practice is to add a specific code to ignore. In order to do this, you need to use: # mypy: [code]
. This comment can be used at the beginning of the file to ignore the code in the whole file.
Error code list: https://mypy.readthedocs.io/en/stable/error_code_list2.html#error-codes-optional
Summary
Mypy is a great tool to improve our code: to make it clearer, cleaner, and more understandable, not only for us but also for other people we work with. In addition, Mypy can show us inconsistency in the code creation process by finding incompatible return values or assignments, etc. Furthermore, it is straightforward to configure and can be easily adjusted to our needs.
Mypy documentation: https://mypy.readthedocs.io/en/stable/
Next is Pylint
Pylint, as the name suggests, is a linter, i.e. a tool to statically analyze the code. The tool checks our code and tries to find issues in it, for instance, missing lines, unused variables, or imports. Furthermore, this linter goes one step further and tries to improve our code by enforcing coding standards. Moreover, it tries to find exuberantly complicated code parts and show us some refactor suggestions. Pylint is easy to set up and has multiple configuration options with external plugins; it may even create some custom checks.
Note: all commands have been checked on the Linux distribution (Debian/Ubuntu).
Usage
To install Pylint, run: pip3 install pylint
Scan a single file: pylint my_code.py
Scan the whole project: find . -iname '*.py' | xargs pylint
To avoid additional warnings, generate a basic configuration file:
pylint --disable=bare-except,invalid-name --class-rgx='[A-Z][a-z]+' --generate-rcfile
Copy the whole output to the .pylintrc
file in the root project directory. The above command will generate a basic configuration list of basic options with the additional options bare-except
and invalid-name
disabled. bare-except
arises when the except clause doesn’t specify the exception type to recognize and invalid-name
when the name doesn’t conform to the naming rules associated with its type.
Sample errors found by Pylint:
Code:
def return_only_a_book(book_a: str, book_b: str) -> str: return book_a
Errors:
my_code.py:4:0: C0304: Final newline missing (missing-final-newline) my_code.py:3:46: W0613: Unused argument 'book_b' (unused-argument)
In this case, Pylint shows us two issues in the code. The first is a missing-final-newline
in line 4, which means that the code is missing a new line in the file. To resolve this issue, we only need to add a new line at the end of the file. The other issue is unused-argument
. This is a more interesting case because we didn’t use an argument of a function, so it may be only our typo or it may signal a logic issue in our function. To resolve it, we need to check again our solution, and decide to remove unnecessary arguments, use it in the method body, or even rewrite the logic, if needed.
Base configuration and ignoring rules
The .pylintrc
file placed in the root project directory is Pylint’s main configuration file. It contains rules like the code style, ignoring rules, refactoring checkers, spelling checks, and more, which will be in global usage. The basic file can be generated by the pylint --disable=bare-except,invalid-name --class-rgx='[A-Z][a-z]+' --generate-rcfile
command.
Here is a list of Pylint rules: https://pylint.pycqa.org/en/latest/technical_reference/features.html. The tools provide a humongous amount of configuration so that we can easily adjust them to our projects.
The most common settings are based on disabling or enabling options, like: disable=invalid-name,raw-checker-failed,file-ignored,useless-suppression
enable=c-extension-no-member,too-many-locals
.
Like in Mypy, in Pylint we can also ignore rules by comments placed in single files.
To ignore rules in the whole file, add: # pylint: disable=rule,
on the top. This comment can contain multiple rule names or code rules to ignore: # pylint: disable=rule, rule, …
.
What is more, “ignore comments” can ignore a single code line by using the same comment and schema: def _success(a: str, b:str): # pylint: disable=W0613
.
Pylint also has an option to enable rules by the # pylint: enable=rule
comment.
Personally and according to the underlying rule “be as descriptive as possible,” I suggest using the full rule name to avoid misunderstanding inside the team. We can disable rules in case when we are working with legacy code or code which is written by inexperienced programmers, and our goal is to add a new functionality. Disabling rules can be very useful to avoid spending an awful lot of time resolving issues related to existing code or integration with existing modules.
Code list: http://pylint-messages.wikidot.com/all-codes
What is more, Pylint can be connected with pre-commit
Git hook, and run checks on every commit; you can find more information about it here: https://pylint.pycqa.org/en/latest/user_guide/pre-commit-integration.html.
Summary
Pylint can list syntax issues, possibly problematic fragments of code, or just find code that can be refactored only by checking pre-compiled code. Similarly to Mypy, the purpose of this tool is to create clearer, cleaner, more understandable, and easy-to-read code. That’s why it is one of the most useful tools in the coding process.
For more information, visit https://pylint.org/.
Black is the last of our three friends
Black is a pretty basic tool that can format your code in a really satisfying way. Anyhow, who likes to format the code manually, or add comments to incorrect indents in pull requests on GitHub or Bitbucket? Generally, it is a waste of precious time which could be spent on more important features or functionalities. Black just formats our Python code with PEP 8, which is a standard defining good practice that provides code guidelines.
Installation: pip3 install black
To only check code style in a single file: black myfile.py --check
To format code in a single file: black myfile.py
To run check globally: black --check .
To format code globally: black *
Looks just perfect. Sadly, codes formatted by Black have several issues. If we have the wrong indent which is the cause of the function not working, Black will also fail.
Like in this case:
setup = self.builder.get_body() car.set_body(setup)
The line with setup = self.builder.get_body()
has incorrect indentation. If we run black .
, Black will display only the line number where the error was found. In this case, we need to fix the indentation manually. Luckily, thanks to Black, we can at least recognize where the bug appeared.
Configuration
In Black, we can create configuration by adding pyproject.toml
file to the root directory. All rules should be placed under the global mark [tool.black]
. To display the list of configuration options, use the command: black --help
.
Sample configuration file
[tool.black] line-length = 100 target-version = ['py37'] include = '\.pyi?$'
Now, if we run Black, it throws warnings to lines with more than 100 characters.
Base configuration options:
target-version = ['py37']
— we directly specify the Python version.include = '\.pyi?$'
— by selecting this option we tell Black to scan Python files.
Furthermore, Black can be run in docker containers (https://black.readthedocs.io/en/stable/usage_and_configuration/black_docker_image.html) and integrated with IDE, Github actions, or version control by pre-commit (https://black.readthedocs.io/en/stable/integrations/index.html)
For more information, visit https://black.readthedocs.io/en/stable/.
Conclusion
All your new friends that you have met here have one main goal. They help you create a better code. They are of great use when it comes to finding common mistakes, typos, and possible problems in the code. Furthermore, they will save a huge amount of your and your colleagues’ time, energy, or frustration. Additionally, the order in which these tools appeared in the article is not fortuitous. I suggest using the aforementioned libraries in this order: Black, Mypy, and Pylint. This setup should optimize the probability of correct and properly formatted code after any fixes or updates. This setup works well for me and other programmers in my teams. I personally use these tools daily in this particular order and they work pretty well, but mix them as much as you need, or just select one of them.
Use your new friends and make your code better!
If you want to work with a team that embraces code standards…
Want to share your content on python-bloggers? click here.