Linting is essential to writing clean and readable code that you can share with others. A linter, like Ruff, is a tool that analyzes your code and looks for errors, stylistic issues, and suspicious constructs. Linting allows you to address issues and improve your code quality before you commit your code and share it with others.
Ruff is a modern linter that’s extremely fast and has a simple interface, making it straightforward to use. It also aims to be a drop-in replacement for many other linting and formatting tools, such as Flake8, isort, and Black. It’s quickly becoming one of the most popular Python linters.
In this tutorial, you’ll learn how to:
- Install Ruff
- Check your Python code for errors
- Automatically fix your linting errors
- Use Ruff to format your code
- Add optional configurations to supercharge your linting
To get the most from this tutorial, you should be familiar with virtual environments, installing third-party modules, and be comfortable with using the terminal.
Ruff cheat sheet: Click here to get access to a free Ruff cheat sheet that summarizes the main Ruff commands you’ll use in this tutorial.
Take the Quiz: Test your knowledge with our interactive “Ruff: A Modern Python Linter” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Ruff: A Modern Python LinterIn this quiz, you'll test your understanding of Ruff, a modern linter for Python. By working through this quiz, you'll revisit why you'd want to use Ruff to check your Python code and how it automatically fixes errors, formats your code, and provides optional configurations to enhance your linting.
Installing Ruff
Now that you know why linting your code is important and how Ruff is a powerful tool for the job, it’s time to install it. Thankfully, Ruff works out of the box, so no complicated installation instructions or configurations are needed to start using it.
Assuming your project is already set up with a virtual environment, you can install Ruff in the following ways:
$ python -m pip install ruff
In addition to pip
, you can also install Ruff with Homebrew if you’re on macOS or Linux:
$ brew install ruff
Conda users can install Ruff using conda-forge
:
$ conda install -c conda-forge ruff
If you use Arch, Alpine, or openSUSE Linux, you can also use the official distribution repositories. You’ll find specific instructions on the Ruff installation page of the official documentation.
Additionally, if you’d like Ruff to be available for all your projects, you might want to install Ruff with pipx
.
You can check that Ruff installed correctly by using the ruff version
command:
$ ruff version
ruff 0.4.7
For the ruff
command to appear in your PATH
, you may need to close and reopen your terminal application or start a new terminal session.
Linting Your Python Code
While linting helps keep your code consistent and error-free, it doesn’t guarantee that your code will be bug-free. Finding the bugs in your code is best handled with a debugger and adequate testing, which won’t be covered in this tutorial. Coming up in the next sections, you’ll learn how to use Ruff to check for errors and speed up your workflow.
Checking for Errors
The code below is a simple script called one_ring.py
. When you run it, it gets a random Lord of the Rings character name from a tuple
and lets you know if that character bore the burden of the One Ring. This code has no real practical use and is just a bit of fun. Regardless of the size of your code base, the steps are going to be the same:
one_ring.py
1import os
2import random
3
4CHARACTERS = ("Frodo", "Sam", "Merry", "Pippin", "Aragorn", "Legolas", "Gimli", "Boromir", "Gandalf", "Saruman", "Sauron")
5
6def random_character():
7 return random.choice(CHARACTERS)
8
9def ring_bearer():
10 return name in ("Frodo", "Sam")
11
12if __name__ == "__main__":
13 character = random_character()
14 if ring_bearer(character):
15 print(f"{character} is a ring bearer")
16 else:
17 print(f"{character} is not a ring bearer")
Now, if you’re eagle-eyed, you may have already spotted some problems with this code. If not, don’t worry, you can use Ruff to find them all.
The most basic command the Ruff CLI (command-line interface) has is check
. By default, this command will check all files in the current directory. For this example, you can run the check
command without any arguments. When you run check
on the above code, it outputs the following:
$ ruff check
one_ring.py:1:8: F401 [*] `os` imported but unused
one_ring.py:10:12: F821 Undefined name `name`
Found 2 errors.
[*] 1 fixable with the `--fix` option.
Success! Ruff found two errors. Not only does it show the file and line numbers of the errors, but it also gives you error codes and messages. In addition, it lets you know that one of the two errors is fixable. Great!
Note: In this example, you only have one file in your directory, one_ring.py
. But if you had more, you could check a single file with ruff check one_ring.py
. And, if you prefer to keep your files in a src/
directory and have multiple nested directories, then ruff check src/
will check all files and subdirectories in your src/
folder.
You can tell Ruff to fix errors by applying the --fix
flag. Here’s what happens when you follow its suggestion:
$ ruff check --fix
one_ring.py:9:12: F821 Undefined name `name`
Found 2 errors (1 fixed, 1 remaining).
The unused import is now fixed, and that line of code has been removed from one_ring.py
. The last of these two errors isn’t automatically fixable. The problem in line 9 may be obvious to you, but maybe it’s not.
Note: Notice how the line number changed for the remaining error? That’s because the unused import was removed, which moved all the code up one line.
Thankfully, Ruff gives you the error code and a way to look it up quickly without having to search the documentation online. Enter the second ruff
command: rule
.
Since Ruff provides the error code, you can pass it to the ruff rule
command to see more details about the error message, including a code example:
$ ruff rule F821
When you run this command, you get more details in Markdown format in your terminal:
# undefined-name (F821)
Derived from the **PyFlakes** linter.
## What it does
Checks for uses of undefined names.
## Why is this bad?
An undefined name is likely to raise `NameError` at runtime.
## Example
```python
def double():
return n * 2 # raises `NameError` if `n` is undefined when `double` is called
```
Use instead:
```python
def double(n):
return n * 2
```
## References
- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)
With the extra context from the error code, you can now see that the example code you saw earlier made the same mistake. The name
variable in line 9 wasn’t passed as an argument to the ring_bearer()
function signature. Whoops!
To fix this error, you can amend ring_bearer()
to take the name
argument:
one_ring.py
# ...
def ring_bearer(name):
return name in ("Frodo", "Sam")
Now that you’ve made that small edit to the code, you can run ruff check
again to see if it passes:
$ ruff check
All checks passed!
Great! Both errors are now fixed, and your code should look like this:
one_ring.py
1import random
2
3CHARACTERS = ("Frodo", "Sam", "Merry", "Pippin", "Aragorn", "Legolas", "Gimli", "Boromir", "Gandalf", "Saruman", "Sauron")
4
5def random_character():
6 return random.choice(CHARACTERS)
7
8def ring_bearer(name):
9 return name in ("Frodo", "Sam")
10
11if __name__ == "__main__":
12 character = random_character()
13 if ring_bearer(character):
14 print(f"{character} is a ring bearer")
15 else:
16 print(f"{character} is not a ring bearer")
Having to run ruff check
every time you change your code can be impractical. Thankfully, Ruff has a solution. In the next section, you’ll look at how you can check your code continuously for errors.
Speeding Up Your Workflow
When you’re actively working on code, Ruff can simplify your workflow even more by informing you of errors as you develop. This will speed up the overall process and make you more productive. To have continuous linting as you code, open a new terminal window and pass the --watch
flag to the check
command:
$ ruff check --watch
After you run the above command, you should see something like this in your terminal:
[14:04:01 PM] Starting linter in watch mode...
[14:04:01 PM] Found 0 errors. Watching for file changes.
Your code is now free from errors. Or is it? In the next section, you’ll learn what Ruff didn’t pick up by default.
Finding More Errors
Even though the errors Ruff found have been fixed, the code still needs to be cleaned up. There are a couple more problems with the one_ring.py
file that could be fixed to make this code even cleaner and more readable. The most notable issue is in line 3. The CHARACTERS
tuple seems too long and could be made more readable.
You may be asking the question, why didn’t Ruff pick that up? This is a perfectly valid question. Digging into the documentation gives this answer:
By default, Ruff enables Flake8’s
F
rules, along with a subset of theE
rules, omitting any stylistic rules that overlap with the use of a formatter, likeruff format
or Black. (Source)
Out-of-the-box Ruff doesn’t apply the rule to check line length. You can, however, tell it which additional rules you want to include or exclude. You can ask it to include all E
rules or a specific rule with the --select
flag:
$ ruff check --select E
one_ring.py:4:89: E501 Line too long (122 > 88)
Found 1 error.
$ ruff check --select E501
one_ring.py:4:89: E501 Line too long (122 > 88)
Found 1 error.
Ah, you found the additional error. However, you may notice that there’s no suggestion to let you know the line length can be automatically fixed with the --fix
flag. Don’t worry because there’s a way to fix formatting errors in Ruff with a new command. In the next section, you’ll learn about ruff format
.
Formatting Your Python Code
By default, Ruff has sensible formatting rules and was designed to be a drop-in replacement for Black. The format
command has been available since Ruff version 0.1.2.
Just like the check
command, the format
command takes optional arguments for a path to a single file or directory. Since the code you have in this tutorial example is a single file, you can go ahead and use it without any arguments:
$ ruff format
1 file reformatted
Your one_ring.py
file should now look more readable and have consistent formatting:
1import random
2
3CHARACTERS = (
4 "Frodo",
5 "Sam",
6 "Merry",
7 "Pippin",
8 "Aragorn",
9 "Legolas",
10 "Gimli",
11 "Boromir",
12 "Gandalf",
13 "Saruman",
14 "Sauron",
15)
16
17
18def random_character():
19 return random.choice(CHARACTERS)
20
21
22def ring_bearer(name):
23 return name in ("Frodo", "Sam")
24
25
26if __name__ == "__main__":
27 character = random_character()
28 if ring_bearer(character):
29 print(f"{character} is a ring bearer")
30 else:
31 print(f"{character} is not a ring bearer")
As you can see, the previous line length error in line 3 has been addressed. And although the tuple takes up more lines, it’s much easier to parse and read the list of character names. This also makes it easier for code reviewers to review changes, as most tools and platforms will only show what has exactly changed in the diff
and not the whole data structure.
The next change it made is that the spacing between functions is now consistent and PEP 8 compliant, with the recommended two spaces between functions.
The last change, although it may seem insignificant, is that Ruff added the missing newline at the end of the file.
This is a short piece of code that was straightforward to format. Longer code bases may need many changes, which could potentially break some functionality, though this is rare as formatters always err on the side of caution. To learn more about unsafe fixes in Ruff, refer to the fix safety section in Ruff’s documentation.
If you’d like to see what changes will be made when you run ruff format
, you can run it with the --diff
flag to see the proposed changes before you make them. If you had run the --diff
flag before running ruff format
, you would’ve seen this output:
--- one_ring.py
+++ one_ring.py
@@ -1,16 +1,31 @@
import random
-CHARACTERS = ("Frodo", "Sam", "Merry", "Pippin", "Aragorn", "Legolas", "Gimli", "Boromir", "Gandalf", "Saruman", "Sauron")
+CHARACTERS = (
+ "Frodo",
+ "Sam",
+ "Merry",
+ "Pippin",
+ "Aragorn",
+ "Legolas",
+ "Gimli",
+ "Boromir",
+ "Gandalf",
+ "Saruman",
+ "Sauron",
+)
+
def random_character():
return random.choice(CHARACTERS)
+
def ring_bearer(name):
return name in ("Frodo", "Sam")
+
if __name__ == "__main__":
character = random_character()
if ring_bearer(character):
print(f"{character} is a ring bearer")
else:
- print(f"{character} is not a ring bearer")
\ No newline at end of file
+ print(f"{character} is not a ring bearer")
1 file would be reformatted
This may be all you ever need to format your code. However, there may be times you’d prefer a different line length or would like to include or exclude certain rules. In these situations, it can be time-consuming to list all your required rules to the command line each time you want to lint your code. There must be a better way!
There is. Although not required, Ruff can be highly configurable. In the next section, you’ll get a brief look into a few configuration basics.
Configuring Ruff
If you’re linting a larger code base, have multiple committers, or want to customize your experience, Ruff allows you to store your configuration in a TOML file. More specifically, a ruff.toml
, .ruff.toml
, or your existing pyproject.toml
file.
As mentioned earlier, ruff
has sensible defaults. These configurations are documented on the Ruff configuration page for you to read. The full list of settings available for your configuration is well documented. Here’s an example of a simple ruff.toml
configuration you can add to your project:
ruff.toml
1line-length = 88
2
3[lint]
4select = ["E501", "I"]
5
6[format]
7docstring-code-format = true
8docstring-code-line-length = 72
And here’s the same example in a pyproject.toml
format. The only change is that you need to include a tool.ruff
prefix in each table header:
pyproject.toml
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E501", "I"]
[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = 72
In these examples, you’ll notice a few new rules. Just as you did earlier, you’ve specifed that you want to include the E501
rule when linting with ruff
, which will return an error when the line length is greater than the default 88 characters.
In addition to adding the E501
rule to the linting configuration, you’ve also asked Ruff to add all the I
rules. I
rules are unique to isort, another package you may have used before to lint and format your Python import
statements. With this configuration, you no longer need isort and Black to format your code. This means fewer tools to manage and fewer developer dependencies.
In lines 6 to 8, you’ll see that Ruff will now format your docstrings to a length of 72 characters. This number could be anything you want it to be, and many might choose 88 characters to match the code line length. Keep in mind that by default, Ruff doesn’t format docstrings.
There are many linting and formatting settings available, so it’s a good idea to scroll through the list of settings to see which ones you want to add to your Ruff configuration.
If you already have experience with a linter, please feel free to share your favorite rules and customizations in the comments below.
Next Steps
Now that you’ve learned why you should use a linter and how Ruff is a great tool to help you achieve clean, readable, and error-free code, you should take Ruff for a spin.
As mentioned above, there are a plethora of configurations you can use to take your linting to the next level. There are also a few integrations that can speed up your workflow, such as the VS Code extension, PyCharm plugin, pre-commit hook, and GitHub Actions.
Conclusion
Ruff is an extremely fast Python linter and code formatter that can help you improve your code quality and maintainability. This tutorial explained how to get started with Ruff, showcased its key features, and demonstrated how powerful it can be.
In this tutorial, you learned how to:
- Install Ruff
- Check your Python code for errors
- Automatically fix your linting errors
- Use Ruff to format your code
- Add optional configurations to supercharge your linting
With this new tool in your toolbox, you’ll be able to take your code to the next level and ensure it looks professional and, more importantly, is error-free.
Ruff cheat sheet: Click here to get access to a free Ruff cheat sheet that summarizes the main Ruff commands you’ll use in this tutorial.
Take the Quiz: Test your knowledge with our interactive “Ruff: A Modern Python Linter” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Ruff: A Modern Python LinterIn this quiz, you'll test your understanding of Ruff, a modern linter for Python. By working through this quiz, you'll revisit why you'd want to use Ruff to check your Python code and how it automatically fixes errors, formats your code, and provides optional configurations to enhance your linting.