
Table of Contents
Recommended Course
Linting is essential to writingclean 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 andimprove your code quality before youcommit 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 asFlake8,isort, andBlack. It’s quickly becoming one of the most popular Python linters.
In this tutorial, you’ll learn how to:
To get the most from this tutorial, you should be familiar withvirtual environments,installing third-party modules, and be comfortable with using theterminal.
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.
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-mpipinstallruffIn addition topip, you can also install Ruff withHomebrew if you’re on macOS or Linux:
$brewinstallruffConda users can install Ruff usingconda-forge:
$condainstall-cconda-forgeruffIf you use Arch, Alpine, or openSUSE Linux, you can also use the official distribution repositories. You’ll find specific instructions on theRuff 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 withpipx.
You can check that Ruff installed correctly by using theruff version command:
$ruffversionruff 0.4.7For theruff command to appear in yourPATH, you may need to close and reopen your terminal application or start a new terminal session.
While linting helps keep your code consistent and error-free, it doesn’t guarantee that your code will bebug-free. Finding the bugs in your code is best handled with adebugger and adequatetesting, 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.
The code below is a simple script calledone_ring.py. When you run it, it gets arandomLord of the Rings character name from atuple and lets you know if that character bore the burden of theOne 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 1importos 2importrandom 3 4CHARACTERS=("Frodo","Sam","Merry","Pippin","Aragorn","Legolas","Gimli","Boromir","Gandalf","Saruman","Sauron") 5 6defrandom_character(): 7returnrandom.choice(CHARACTERS) 8 9defring_bearer():10returnnamein("Frodo","Sam")1112if__name__=="__main__":13character=random_character()14ifring_bearer(character):15print(f"{character} is a ring bearer")16else:17print(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 ischeck. By default, this command will check all files in the current directory. For this example, you can run thecheck command without any arguments. When you runcheck on the above code, it outputs the following:
$ruffcheckone_ring.py:1:8: F401 [*] `os` imported but unusedone_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 withruff check one_ring.py. And, if you prefer to keep your files in asrc/ directory and have multiple nested directories, thenruff check src/ will check all files and subdirectories in yoursrc/ folder.
You can tell Ruff to fix errors by applying the--fix flag. Here’s what happens when you follow its suggestion:
$ruffcheck--fixone_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 fromone_ring.py. The last of these two errors isn’t automatically fixable. The problem inline 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 secondruff command:rule.
Since Ruff provides the error code, you can pass it to theruff rule command to see more details about the error message, including a code example:
$ruffruleF821When you run this command, you get more details in Markdown format in your terminal:
# undefined-name (F821)Derived from the**PyFlakes** linter.## What it doesChecks for uses of undefined names.## Why is this bad?An undefined name is likely to raise`NameError` at runtime.## Example```pythondefdouble():returnn*2# raises `NameError` if `n` is undefined when `double` is called```Use instead:```pythondefdouble(n):returnn*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. Thename variable in line 9 wasn’t passed as an argument to thering_bearer() function signature. Whoops!
To fix this error, you can amendring_bearer() to take thename argument:
one_ring.py# ...defring_bearer(name): returnnamein("Frodo","Sam")Now that you’ve made that small edit to the code, you can runruff check again to see if it passes:
$ruffcheckAll checks passed!Great! Both errors are now fixed, and your code should look like this:
one_ring.py 1importrandom 2 3CHARACTERS=("Frodo","Sam","Merry","Pippin","Aragorn","Legolas","Gimli","Boromir","Gandalf","Saruman","Sauron") 4 5defrandom_character(): 6returnrandom.choice(CHARACTERS) 7 8defring_bearer(name): 9returnnamein("Frodo","Sam")1011if__name__=="__main__":12character=random_character()13ifring_bearer(character):14print(f"{character} is a ring bearer")15else:16print(f"{character} is not a ring bearer")Having to runruff 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.
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 havecontinuous linting as you code, open a new terminal window and pass the--watch flag to thecheck command:
$ruffcheck--watchAfter 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.
Even though the errors Ruff found have been fixed, the code still needs to be cleaned up. There are a couple more problems with theone_ring.py file that could be fixed to make this code evencleaner and more readable. The most notable issue is inline 3. TheCHARACTERS 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
Frules, along with a subset of theErules, omitting any stylistic rules that overlap with the use of a formatter, likeruff formator Black. (Source)
Out-of-the-box Ruff doesn’t apply the rule to check line length. You can, however, tell it whichadditional rules you want to include or exclude. You can ask it to include allE rules or a specific rule with the--select flag:
$ruffcheck--selectEone_ring.py:4:89: E501 Line too long (122 > 88)Found 1 error.$ruffcheck--selectE501one_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 aboutruff format.
By default, Ruff has sensible formatting rules and was designed to be adrop-in replacement for Black. Theformat command has been available sinceRuff version 0.1.2.
Just like thecheck command, theformat 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:
$ruffformat1 file reformattedYourone_ring.py file should now look more readable and have consistent formatting:
1importrandom 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)161718defrandom_character():19returnrandom.choice(CHARACTERS)202122defring_bearer(name):23returnnamein("Frodo","Sam")242526if__name__=="__main__":27character=random_character()28ifring_bearer(character):29print(f"{character} is a ring bearer")30else:31print(f"{character} is not a ring bearer")As you can see, the previous line length error inline 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 thediff and not the whole data structure.
The next change it made is that the spacing between functions is now consistent andPEP 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 aboutunsafe fixes in Ruff, refer to thefix safety section in Ruff’s documentation.
If you’d like to see what changes will be made when you runruff 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 runningruff 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 reformattedThis 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 behighly configurable. In the next section, you’ll get a brief look into a few configuration basics.
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 aTOML file. More specifically, aruff.toml,.ruff.toml, or your existingpyproject.toml file.
As mentioned earlier,ruff has sensible defaults. These configurations are documented on theRuff configuration page for you to read. The fulllist of settings available for your configuration is well documented. Here’s an example of a simpleruff.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=72And here’s the same example in apyproject.toml format. The only change is that you need to include atool.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=truedocstring-code-line-length=72In these examples, you’ll notice a few new rules. Just as you did earlier, you’ve specifed that you want to include theE501 rule when linting withruff, which will return an error when the line length is greater than the default 88 characters.
In addition to adding theE501 rule to the linting configuration, you’ve also asked Ruff to add all theI rules.I rules are unique to isort, another package you may have used before to lint and format your Pythonimport 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.
Inlines 6 to 8, you’ll see that Ruff will now format yourdocstrings 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.
Now that you’ve learned why you should use a linter and how Ruff is a great tool to help youachieve 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 alsoa few integrations that canspeed up your workflow, such as theVS Code extension,PyCharm plugin,pre-commit hook, andGitHub Actions.
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:
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.
Recommended Course
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

AboutRicky White
Ricky is a software engineer and writer from a non-traditional background. He's an enthusiastic problem solver with passion for creating and building, from software and websites to books and bonsai.
» More about RickyMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.
Keep Learning
Already have an account?Sign-In
Almost there! Complete this form and click the button below to gain instant access:

Ruff: A Modern Python Linter for Error-Free and Maintainable Code (Cheat Sheet)
Get aPython Cheat Sheet (PDF) and learn the basics of Python, like working with data types, dictionaries, lists, and Python functions:
