<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marcos Alonso Developer Blog]]></title><description><![CDATA[Explore articles on full-stack development, Python, and tech leadership from a Head of Software Development. Build better software and grow your career.]]></description><link>https://blog.marcosalonso.dev</link><image><url>https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/logos/693f12841b3e34ceb8539e5b/61086d59-ed61-404a-99b1-f2df4fadc40b.png</url><title>Marcos Alonso Developer Blog</title><link>https://blog.marcosalonso.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 20:54:34 GMT</lastBuildDate><atom:link href="https://blog.marcosalonso.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Complete Python Code Quality Stack in 2026: Ruff + mypy]]></title><description><![CDATA[The Complete Python Code Quality Stack in 2026: Ruff + mypy
If you're starting a professional Python project today, you only need two tools to cover linting, formatting, and type checking: Ruff and my]]></description><link>https://blog.marcosalonso.dev/the-complete-python-code-quality-stack-in-2026-ruff-mypy</link><guid isPermaLink="true">https://blog.marcosalonso.dev/the-complete-python-code-quality-stack-in-2026-ruff-mypy</guid><category><![CDATA[Python]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[codequality]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[ruff]]></category><category><![CDATA[Mypy]]></category><category><![CDATA[typing]]></category><category><![CDATA[VS Code]]></category><category><![CDATA[AI]]></category><category><![CDATA[UV ]]></category><category><![CDATA[Poetry]]></category><dc:creator><![CDATA[Marcos Alonso]]></dc:creator><pubDate>Thu, 12 Mar 2026 12:30:56 GMT</pubDate><content:encoded><![CDATA[<h1>The Complete Python Code Quality Stack in 2026: Ruff + mypy</h1>
<p>If you're starting a professional Python project today, you only need two tools to cover linting, formatting, and type checking: <strong>Ruff</strong> and <strong>mypy</strong>. Together they replace an entire ecosystem — flake8, dozens of flake8 plugins, isort, Black, pyupgrade, and pylint — while being faster and easier to configure.</p>
<p>This guide covers the complete setup: Ruff as both linter and formatter, mypy for type checking, how they complement each other, and every rule worth enabling with concrete examples.</p>
<h2>The big picture: what does each tool do?</h2>
<p>Before Python had Ruff, a professional project needed a pile of tools:</p>
<table>
<thead>
<tr>
<th>Concern</th>
<th>Old tools</th>
<th>Modern replacement</th>
</tr>
</thead>
<tbody><tr>
<td>Linting</td>
<td>flake8 + 20 plugins, pylint</td>
<td><code>ruff check</code></td>
</tr>
<tr>
<td>Import sorting</td>
<td>isort</td>
<td><code>ruff check</code> (rule <code>I</code>)</td>
</tr>
<tr>
<td>Code formatting</td>
<td>Black</td>
<td><code>ruff format</code></td>
</tr>
<tr>
<td>Syntax modernization</td>
<td>pyupgrade</td>
<td><code>ruff check</code> (rule <code>UP</code>)</td>
</tr>
<tr>
<td>Type checking</td>
<td>mypy</td>
<td>mypy (still the best)</td>
</tr>
</tbody></table>
<p>Ruff handles linting <strong>and</strong> formatting in a single binary. mypy handles type checking. There is some overlap — but they complement each other rather than compete.</p>
<h3>Why not just Ruff?</h3>
<p>Ruff's linter can catch some type-related issues (like <code>UP</code> for modern type syntax or <code>FBT</code> for boolean traps), but it does <strong>static analysis on a per-file basis</strong>. It doesn't understand your program's types across modules.</p>
<p>mypy performs <strong>cross-file type inference</strong>. It knows that if <code>get_user()</code> returns <code>User | None</code>, you must handle the <code>None</code> case before accessing <code>.name</code>. Ruff can't do this.</p>
<p><strong>You need both.</strong></p>
<hr />
<h2>Part 1: Ruff as formatter</h2>
<p><code>ruff format</code> is a drop-in replacement for Black. It produces nearly identical output and is significantly faster.</p>
<pre><code class="language-toml">[tool.ruff]
target-version = "py313"  # your project's minimum Python version
line-length = 88           # same default as Black
</code></pre>
<p>That's it for the formatter configuration. Run it with:</p>
<pre><code class="language-bash">ruff format .              # format all files
ruff format --check .      # check without modifying (useful in CI)
</code></pre>
<h3>What the formatter does</h3>
<ul>
<li><p>Enforces consistent indentation, quotes, trailing commas, and line breaks</p>
</li>
<li><p>Wraps long lines at the configured <code>line-length</code></p>
</li>
<li><p>Adds trailing commas to multi-line structures (so git diffs are cleaner)</p>
</li>
<li><p>Normalizes string quotes to double quotes by default</p>
</li>
</ul>
<h3>Formatter vs linter: don't overlap</h3>
<p>Because Ruff handles both formatting and linting, some lint rules overlap with the formatter. This is why Ruff only enables <code>E4</code>, <code>E7</code>, <code>E9</code> by default — the remaining <code>E</code> rules (like <code>E1</code> for indentation) are already handled by the formatter. If you enable the full <code>E</code> prefix alongside <code>ruff format</code>, they won't conflict — Ruff is designed to handle this — but the formatter will fix most stylistic issues before the linter even sees them.</p>
<p>Similarly, the <code>Q</code> (quotes), <code>W</code> (whitespace), and <code>COM</code> (trailing commas) lint rules overlap with the formatter. <code>Q</code> and <code>W</code> are fine to enable as a safety net, but <code>COM</code> should be skipped since the formatter handles trailing commas.</p>
<hr />
<h2>Part 2: Ruff as linter — <code>select</code> vs <code>extend-select</code></h2>
<p>Before diving into the rules, let's clarify a common source of confusion.</p>
<p>Ruff provides two ways to configure which lint rules are active:</p>
<pre><code class="language-toml">[tool.ruff.lint]
select = ["E", "F", "B"]         # replaces the defaults
extend-select = ["B", "I", "S"]  # adds to the defaults
</code></pre>
<ul>
<li><p><code>select</code> completely <strong>replaces</strong> the default rule set. If you write <code>select = ["B"]</code>, you lose <code>E4</code>, <code>E7</code>, <code>E9</code>, and <code>F</code> entirely.</p>
</li>
<li><p><code>extend-select</code> <strong>adds</strong> rules on top of the defaults. The defaults (<code>E4</code>, <code>E7</code>, <code>E9</code>, <code>F</code>) remain active.</p>
</li>
</ul>
<p><strong>Always use</strong> <code>extend-select</code><strong>.</strong> It's safer because you'll never accidentally drop the defaults, and if a future Ruff version adds new default rules, you'll get them automatically.</p>
<p>The same logic applies to <code>ignore</code> vs <code>extend-ignore</code>: prefer <code>extend-ignore</code> when adding to existing ignores, though in practice most projects just use <code>ignore</code> since there are no default ignores to preserve.</p>
<h2>The full lint configuration</h2>
<p>Here's the complete <code>extend-select</code> I recommend for any professional Python application:</p>
<pre><code class="language-toml">[tool.ruff.lint]
extend-select = [
    "A",     # flake8-builtins
    "ARG",   # flake8-unused-arguments
    "ASYNC", # flake8-async
    "B",     # flake8-bugbear
    "BLE",   # flake8-blind-except
    "C4",    # flake8-comprehensions
    "C90",   # mccabe
    "DTZ",   # flake8-datetimez
    "E",     # pycodestyle errors (full set)
    "EM",    # flake8-errmsg
    "FBT",   # flake8-boolean-trap
    "FLY",   # flynt
    "FURB",  # refurb
    "G",     # flake8-logging-format
    "I",     # isort
    "ICN",   # flake8-import-conventions
    "ISC",   # flake8-implicit-str-concat
    "INT",   # flake8-gettext
    "LOG",   # flake8-logging
    "N",     # pep8-naming
    "PERF",  # perflint
    "PGH",   # pygrep-hooks
    "PIE",   # flake8-pie
    "PL",    # pylint
    "PTH",   # flake8-use-pathlib
    "Q",     # flake8-quotes
    "RET",   # flake8-return
    "RSE",   # flake8-raise
    "RUF",   # ruff-specific rules
    "S",     # flake8-bandit
    "SIM",   # flake8-simplify
    "SLF",   # flake8-self
    "SLOT",  # flake8-slots
    "T10",   # flake8-debugger
    "T20",   # flake8-print
    "TC",    # flake8-type-checking
    "TID",   # flake8-tidy-imports
    "TRY",   # tryceratops
    "UP",    # pyupgrade
    "W",     # pycodestyle warnings
    "YTT",   # flake8-2020
]
</code></pre>
<p>That's 41 rule prefixes. Let's go through every single one.</p>
<hr />
<h2>Rules enabled by default</h2>
<p>Ruff only enables <code>F</code> (all pyflakes rules) and a subset of <code>E</code> (<code>E4</code>, <code>E7</code>, <code>E9</code>) by default. The rest of <code>E</code> and all of <code>W</code> are <strong>not</strong> enabled — Ruff omits the stylistic rules that overlap with formatters.</p>
<p>This is why our <code>extend-select</code> includes <code>"E"</code> and <code>"W"</code> explicitly: to activate the full set of pycodestyle checks.</p>
<h3><code>F</code> — pyflakes (enabled by default)</h3>
<p>Catches actual code problems, not just style: unused imports, undefined names, redefined unused variables, <code>import *</code> issues. Fully enabled by default.</p>
<pre><code class="language-python">import os  # F401: 'os' imported but unused

x = 1
x = 2  # F841: local variable 'x' is assigned to but never used
</code></pre>
<h3><code>E</code> — pycodestyle errors (partially enabled by default)</h3>
<p>PEP 8 style errors: wrong indentation, missing whitespace around operators, lines too long, unexpected spaces. <strong>Only</strong> <code>E4</code><strong>,</strong> <code>E7</code><strong>, and</strong> <code>E9</code> <strong>are enabled by default.</strong> Adding <code>"E"</code> to <code>extend-select</code> activates the remaining rules (like <code>E1</code>, <code>E2</code>, <code>E5</code>).</p>
<pre><code class="language-python"># E111 (E1xx — NOT enabled by default): indentation is not a multiple of four
if True:
  pass

# E712 (E7xx — enabled by default): comparison to True
if x == True:
    pass
</code></pre>
<h3><code>W</code> — pycodestyle warnings (NOT enabled by default)</h3>
<p>Style warnings: trailing whitespace, blank lines at end of file, deprecated features. Must be explicitly enabled.</p>
<pre><code class="language-python"># W291: trailing whitespace
x = 1

# W292: no newline at end of file
</code></pre>
<hr />
<h2>Code quality and bugs</h2>
<h3><code>B</code> — flake8-bugbear</h3>
<p>Catches common Python gotchas that aren't technically syntax errors but are almost always bugs.</p>
<pre><code class="language-python"># B006: mutable default argument
def add_item(item, items=[]):
    items.append(item)
    return items

# B904: within an except clause, raise from the caught exception
try:
    do_something()
except ValueError:
    raise RuntimeError("failed")  # should be: raise RuntimeError("failed") from e
</code></pre>
<h3><code>C4</code> — flake8-comprehensions</h3>
<p>Suggests simpler, more Pythonic comprehensions and constructor calls.</p>
<pre><code class="language-python"># C400: unnecessary generator, use a list comprehension
list(x for x in range(10))  # -&gt; [x for x in range(10)]

# C416: unnecessary dict comprehension, use dict()
{k: v for k, v in pairs}  # -&gt; dict(pairs)
</code></pre>
<h3><code>C90</code> — mccabe</h3>
<p>Measures cyclomatic complexity — the number of independent paths through a function. When a function has too many nested <code>if</code>/<code>for</code>/<code>try</code> blocks, it becomes hard to understand and test. The default threshold is 10.</p>
<pre><code class="language-python"># C901: function is too complex (15 &gt; 10)
def process(data):
    if data:
        for item in data:
            if item.valid:
                try:
                    if item.type == "a":
                        ...
                    elif item.type == "b":
                        ...
                    # ... many more branches
</code></pre>
<h3><code>PL</code> — pylint</h3>
<p>A portable subset of pylint's rules, reimplemented in Ruff for speed. Covers things like too many arguments, too many branches, comparisons with <code>None</code>, and unused loop variables.</p>
<pre><code class="language-python"># PLR0913: too many arguments in function definition (6 &gt; 5)
def create_user(name, email, age, role, team, department):
    ...

# PLC0208: use a sequence type instead of a set when iterating
for x in {1, 2, 3}:
    print(x)
</code></pre>
<h3><code>FURB</code> — refurb</h3>
<p>Suggests modern Python idioms and API usage. Catches patterns that can be replaced with newer, cleaner alternatives.</p>
<pre><code class="language-python"># FURB118: use operator.itemgetter instead of a lambda
sorted(items, key=lambda x: x[0])  # -&gt; sorted(items, key=operator.itemgetter(0))
</code></pre>
<h3><code>PIE</code> — flake8-pie</h3>
<p>Catches miscellaneous anti-patterns: unnecessary <code>pass</code>, no-effect expressions, redundant re-imports.</p>
<pre><code class="language-python"># PIE790: unnecessary pass statement
class MyError(Exception):
    pass  # unnecessary if the class has a docstring

# PIE804: unnecessary dict kwargs
foo(**{"bar": 1})  # -&gt; foo(bar=1)
</code></pre>
<h3><code>SIM</code> — flake8-simplify</h3>
<p>Simplifies conditionals, context managers, and boolean expressions.</p>
<pre><code class="language-python"># SIM108: use ternary operator instead of if-else block
if condition:
    x = 1
else:
    x = 2
# -&gt; x = 1 if condition else 2

# SIM110: use any() instead of a for loop
for item in items:
    if item.valid:
        return True
return False
# -&gt; return any(item.valid for item in items)
</code></pre>
<h3><code>PERF</code> — perflint</h3>
<p>Catches performance anti-patterns: unnecessary list copies, inefficient loops, redundant conversions.</p>
<pre><code class="language-python"># PERF401: use a list comprehension instead of appending in a for loop
result = []
for x in data:
    result.append(x * 2)
# -&gt; result = [x * 2 for x in data]
</code></pre>
<hr />
<h2>Security</h2>
<h3><code>S</code> — flake8-bandit</h3>
<p>The most important rule set for security-conscious projects. Detects hardcoded passwords, <code>eval()</code>, <code>exec()</code>, <code>pickle</code> usage, <code>subprocess</code> with <code>shell=True</code>, insecure TLS, and many more OWASP-relevant patterns.</p>
<pre><code class="language-python"># S105: possible hardcoded password
password = "admin123"

# S603: subprocess call with shell=True
subprocess.call(cmd, shell=True)

# S301: pickle can execute arbitrary code
data = pickle.loads(raw)
</code></pre>
<h3><code>BLE</code> — flake8-blind-except</h3>
<p>Forbids bare <code>except Exception</code> and <code>except BaseException</code> without re-raising. Forces you to catch specific exceptions.</p>
<pre><code class="language-python"># BLE001: do not catch blind exception BaseException
try:
    do_something()
except Exception:
    pass  # silently swallowing ALL exceptions
</code></pre>
<h3><code>DTZ</code> — flake8-datetimez</h3>
<p>Forbids creating <code>datetime</code> objects without an explicit timezone. Prevents subtle bugs when mixing naive and timezone-aware datetimes.</p>
<pre><code class="language-python"># DTZ001: datetime.datetime() called without a tzinfo argument
datetime.datetime(2026, 1, 1)

# DTZ005: datetime.now() called without a tz argument
datetime.datetime.now()  # -&gt; datetime.datetime.now(tz=datetime.UTC)
</code></pre>
<h3><code>T10</code> — flake8-debugger</h3>
<p>Catches <code>breakpoint()</code>, <code>pdb.set_trace()</code>, and other debugger calls left in the code. These should never reach production.</p>
<pre><code class="language-python"># T100: debugger call found
breakpoint()
import pdb; pdb.set_trace()
</code></pre>
<hr />
<h2>Imports and organization</h2>
<h3><code>I</code> — isort</h3>
<p>Sorts imports following PEP 8 conventions: standard library first, then third-party, then local imports. Highly configurable.</p>
<pre><code class="language-python"># Before isort
import requests
import os
from myapp import utils
import sys

# After isort
import os
import sys

import requests

from myapp import utils
</code></pre>
<h3><code>TID</code> — flake8-tidy-imports</h3>
<p>Bans relative imports, specific banned imports, and incorrect lazy import patterns. Relative imports make refactoring harder and create circular import issues.</p>
<pre><code class="language-python"># TID252: relative imports are banned
from . import utils       # -&gt; from myapp import utils
from ..models import User  # -&gt; from myapp.models import User
</code></pre>
<h3><code>ICN</code> — flake8-import-conventions</h3>
<p>Enforces standard import aliases. The Python ecosystem has strong conventions (<code>np</code>, <code>pd</code>, <code>plt</code>) and this rule enforces them.</p>
<pre><code class="language-python"># ICN001: conventional import alias
import numpy as npy  # should be: import numpy as np
import pandas as frame  # should be: import pandas as pd
</code></pre>
<h3><code>TC</code> — flake8-type-checking</h3>
<p>Moves imports used only in type annotations into an <code>if TYPE_CHECKING</code> block. This reduces application startup time since those imports aren't loaded at runtime. Previously called <code>TCH</code>, renamed to <code>TC</code> in Ruff v0.8.0.</p>
<pre><code class="language-python"># TC001: move import into TYPE_CHECKING block
from myapp.models import User  # only used in type hints

def get_user() -&gt; User:
    ...

# Should be:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from myapp.models import User
</code></pre>
<hr />
<h2>Types and annotations</h2>
<h3><code>UP</code> — pyupgrade</h3>
<p>Modernizes type annotation syntax and other Python constructs to use the latest available syntax for your target Python version.</p>
<pre><code class="language-python"># UP007: use X | Y for union types (Python 3.10+)
Optional[str]  # -&gt; str | None
Union[int, str]  # -&gt; int | str

# UP006: use builtin type for type hints (Python 3.9+)
List[int]  # -&gt; list[int]
Dict[str, Any]  # -&gt; dict[str, Any]
</code></pre>
<h3><code>FBT</code> — flake8-boolean-trap</h3>
<p>Detects boolean positional arguments in function signatures. Booleans as positional args make call sites unreadable.</p>
<pre><code class="language-python"># FBT001: boolean positional argument in function definition
def process(data, verbose):  # what does True mean here?
    ...
process(data, True)  # unclear

# Fix: use keyword-only argument
def process(data, *, verbose):
    ...
process(data, verbose=True)  # clear
</code></pre>
<h3><code>YTT</code> — flake8-2020</h3>
<p>Detects incorrect comparisons against <code>sys.version</code> and <code>sys.version_info</code>. These bugs are subtle and often go unnoticed.</p>
<pre><code class="language-python"># YTT101: sys.version compared with string, use sys.version_info
if sys.version &gt;= "3":  # wrong: "3" &lt; "3.10" in string comparison!
    ...
</code></pre>
<hr />
<h2>Error handling and exceptions</h2>
<h3><code>TRY</code> — tryceratops</h3>
<p>Best practices for exception handling: prefer custom exception classes, don't create long inline messages, don't <code>except</code> and <code>raise</code> without adding context.</p>
<pre><code class="language-python"># TRY003: avoid specifying long messages outside the exception class
raise ValueError("User with id {user_id} was not found in database {db}")
# Better: raise UserNotFoundError(user_id, db)

# TRY301: abstract raise to an inner function
try:
    do_complex_thing()
    raise ValueError("x")  # raise inside try body is an anti-pattern
except ValueError:
    ...
</code></pre>
<h3><code>EM</code> — flake8-errmsg</h3>
<p>Forces error messages to be defined as variables instead of inline strings. This improves tracebacks (the message doesn't appear twice) and avoids duplication.</p>
<pre><code class="language-python"># EM101: exception must not use a string literal
raise ValueError("invalid input")

# Fix:
msg = "invalid input"
raise ValueError(msg)
</code></pre>
<h3><code>RSE</code> — flake8-raise</h3>
<p>Detects unnecessary raise patterns: <code>raise Exception()</code> without arguments, redundant <code>raise e</code>.</p>
<pre><code class="language-python"># RSE102: unnecessary parentheses on raised exception
raise ValueError()  # -&gt; raise ValueError
</code></pre>
<hr />
<h2>Strings and logging</h2>
<h3><code>FLY</code> — flynt</h3>
<p>Converts string concatenation and <code>.format()</code> calls to f-strings.</p>
<pre><code class="language-python"># FLY002: consider f-string instead of string join
" ".join(["hello", name])  # -&gt; f"hello {name}"

"Hello {}".format(name)  # -&gt; f"Hello {name}"
</code></pre>
<h3><code>ISC</code> — flake8-implicit-str-concat</h3>
<p>Detects implicit string concatenation where two strings sit next to each other without an operator. This is usually an accidental bug, like forgetting a comma in a list.</p>
<pre><code class="language-python"># ISC001: implicitly concatenated string on one line
x = "foo" "bar"  # did you mean "foobar" or ["foo", "bar"]?

# Common real-world bug:
items = [
    "apple"
    "banana",  # oops — "apple" and "banana" merged into "applebanana"
    "cherry",
]
</code></pre>
<h3><code>G</code> — flake8-logging-format</h3>
<p>Forbids f-strings and <code>.format()</code> in logging calls. Loggers should use <code>%s</code> lazy formatting so the string is only built if the message is actually emitted.</p>
<pre><code class="language-python"># G004: logging statement uses f-string
logging.info(f"User {user.id} logged in")
# -&gt; logging.info("User %s logged in", user.id)
</code></pre>
<h3><code>LOG</code> — flake8-logging</h3>
<p>Best practices for logging: use <code>logger</code> instances instead of the <code>logging</code> module directly, correct log levels, proper logger naming.</p>
<pre><code class="language-python"># LOG001: use logging.getLogger() to get a logger
logging.info("message")  # should use: logger = logging.getLogger(__name__)

# LOG009: use logging.WARN is deprecated, use WARNING
logging.WARN  # -&gt; logging.WARNING
</code></pre>
<h3><code>Q</code> — flake8-quotes</h3>
<p>Enforces consistent quote style (single or double) across the project. If you're using <code>ruff format</code>, this is already handled automatically — but it's useful as a safety net in CI.</p>
<pre><code class="language-python"># Q000: double quotes found but single quotes preferred
x = "hello"  # -&gt; x = 'hello' (or vice versa, depending on config)
</code></pre>
<h3><code>INT</code> — flake8-gettext</h3>
<p>Detects issues with internationalization strings (<code>_()</code>, <code>ngettext()</code>). Only relevant if your application supports multiple languages.</p>
<pre><code class="language-python"># INT001: f-string is resolved before function call; consider _(...)
_(f"Hello {name}")  # the f-string resolves before _() can translate it
# -&gt; _("Hello %s") % name
</code></pre>
<hr />
<h2>Style and cleanup</h2>
<h3><code>N</code> — pep8-naming</h3>
<p>Enforces PEP 8 naming conventions: <code>snake_case</code> for functions and variables, <code>PascalCase</code> for classes, <code>UPPER_CASE</code> for module-level constants.</p>
<pre><code class="language-python"># N801: class name should use CapWords convention
class my_class:  # -&gt; class MyClass
    pass

# N802: function name should be lowercase
def MyFunction():  # -&gt; def my_function()
    pass
</code></pre>
<h3><code>T20</code> — flake8-print</h3>
<p>Detects <code>print()</code> calls in the codebase. In a professional application, you should use <code>logging</code> instead. Print statements are for debugging and should not reach production.</p>
<pre><code class="language-python"># T201: print found
print("Processing...")  # -&gt; logging.info("Processing...")
</code></pre>
<h3><code>A</code> — flake8-builtins</h3>
<p>Prevents shadowing Python built-in names. Shadowing builtins leads to confusing bugs.</p>
<pre><code class="language-python"># A001: variable is shadowing a Python builtin
list = [1, 2, 3]  # now list() constructor is broken
id = 42            # now id() function is broken
type = "admin"     # now type() function is broken
</code></pre>
<h3><code>ARG</code> — flake8-unused-arguments</h3>
<p>Detects function arguments that are never used in the function body.</p>
<pre><code class="language-python"># ARG001: unused function argument
def process(data, verbose):  # 'verbose' is never used
    return data.transform()
</code></pre>
<h3><code>RET</code> — flake8-return</h3>
<p>Simplifies return statements: removes unnecessary <code>else</code> after <code>return</code>, detects inconsistent returns.</p>
<pre><code class="language-python"># RET505: unnecessary else after return
def check(x):
    if x &gt; 0:
        return True
    else:  # unnecessary — the if already returned
        return False
# -&gt; remove the else
</code></pre>
<h3><code>PTH</code> — flake8-use-pathlib</h3>
<p>Suggests <code>pathlib.Path</code> over <code>os.path</code> functions. <code>pathlib</code> is more readable and Pythonic.</p>
<pre><code class="language-python"># PTH118: os.path.join should be replaced by Path with / operator
os.path.join("dir", "file.txt")  # -&gt; Path("dir") / "file.txt"

# PTH123: open() should be replaced by Path.open()
open("file.txt")  # -&gt; Path("file.txt").open()
</code></pre>
<h3><code>SLF</code> — flake8-self</h3>
<p>Detects access to private members (<code>_name</code>) from outside the class that defines them. Private members are an implementation detail and shouldn't be accessed externally.</p>
<pre><code class="language-python"># SLF001: private member accessed
obj._internal_method()  # don't touch private members from outside
</code></pre>
<h3><code>SLOT</code> — flake8-slots</h3>
<p>Suggests adding <code>__slots__</code> to classes that inherit from immutable types like <code>str</code>, <code>int</code>, <code>tuple</code>. This reduces memory usage.</p>
<pre><code class="language-python"># SLOT000: subclass of str should define __slots__
class Color(str):
    pass
# -&gt; class Color(str):
#        __slots__ = ()
</code></pre>
<h3><code>PGH</code> — pygrep-hooks</h3>
<p>Catches sloppy suppression comments: <code>type: ignore</code> without a specific error code, <code>noqa</code> without a specific rule code, and <code>eval()</code> in strings.</p>
<pre><code class="language-python"># PGH003: use specific rule codes with type: ignore
x: int = "hello"  # type: ignore     # bad
x: int = "hello"  # type: ignore[assignment]  # good
</code></pre>
<h3><code>RUF</code> — ruff-specific rules</h3>
<p>Rules unique to Ruff: unused <code>noqa</code> directives, implicit type conversions, mutable collection literals used as class variables, and more.</p>
<pre><code class="language-python"># RUF005: consider unpacking instead of concatenation
x = [1, 2] + [3, 4]  # -&gt; x = [1, 2, *[3, 4]]

# RUF012: mutable class variables must be annotated with ClassVar
class Config:
    items = []  # -&gt; items: ClassVar[list] = []
</code></pre>
<hr />
<h2>Async</h2>
<h3><code>ASYNC</code> — flake8-async</h3>
<p>Detects async anti-patterns: using <code>sleep(0)</code> instead of <code>checkpoint()</code>, blocking the event loop with synchronous I/O, missing timeouts on network calls.</p>
<pre><code class="language-python"># ASYNC100: trio/anyio call without cancellation point
async def fetch():
    await trio.sleep(0)  # should use trio.checkpoint()

# ASYNC210: blocking HTTP call in async function
async def get_data():
    requests.get("https://example.com")  # blocks the event loop!
    # -&gt; use httpx or aiohttp instead
</code></pre>
<hr />
<h2>Rules I deliberately exclude from Ruff (depending on the project)</h2>
<p>Not every rule set belongs in every project. Here's what I leave out and why:</p>
<table>
<thead>
<tr>
<th>Code</th>
<th>Plugin</th>
<th>Why I skip it</th>
</tr>
</thead>
<tbody><tr>
<td><code>D</code></td>
<td>pydocstyle</td>
<td>Too noisy for an application. Forcing docstrings on every private function doesn't improve code quality. If you do want it, use <code>convention = "google"</code> with selective ignores.</td>
</tr>
<tr>
<td><code>ANN</code></td>
<td>flake8-annotations</td>
<td>Redundant if you use <code>mypy --strict</code>, which already enforces type annotations everywhere. Running both creates duplicate noise.</td>
</tr>
<tr>
<td><code>ERA</code></td>
<td>flake8-eradicate</td>
<td>Too many false positives. Legitimate comments are flagged as "commented-out code."</td>
</tr>
<tr>
<td><code>PT</code></td>
<td>flake8-pytest-style</td>
<td>Only relevant if your project uses pytest. Add it if you do.</td>
</tr>
<tr>
<td><code>DJ</code></td>
<td>flake8-django</td>
<td>Django-specific rules. Only for Django projects.</td>
</tr>
<tr>
<td><code>NPY</code></td>
<td>numpy-specific</td>
<td>Only for projects using NumPy.</td>
</tr>
<tr>
<td><code>PD</code></td>
<td>pandas-vet</td>
<td>Only for projects using Pandas.</td>
</tr>
<tr>
<td><code>AIR</code></td>
<td>airflow-specific</td>
<td>Only for Airflow DAGs.</td>
</tr>
<tr>
<td><code>FAST</code></td>
<td>fastapi-specific</td>
<td>Only for FastAPI projects.</td>
</tr>
<tr>
<td><code>COM</code></td>
<td>flake8-commas</td>
<td>The Ruff formatter already handles trailing commas. Enabling both creates conflicts.</td>
</tr>
<tr>
<td><code>INP</code></td>
<td>flake8-no-pep420</td>
<td>Flags packages without <code>__init__.py</code>. Irrelevant if you use namespace packages (PEP 420).</td>
</tr>
<tr>
<td><code>FA</code></td>
<td>flake8-future-annotations</td>
<td>Only useful if you support Python &lt; 3.10 and need <code>from __future__ import annotations</code>.</td>
</tr>
<tr>
<td><code>CPY</code></td>
<td>flake8-copyright</td>
<td>Enforces copyright headers. Corporate-specific, not universal.</td>
</tr>
<tr>
<td><code>TD</code>/<code>FIX</code></td>
<td>flake8-todos/fixme</td>
<td>Flags TODO and FIXME comments. TODOs are a normal part of development.</td>
</tr>
<tr>
<td><code>EXE</code></td>
<td>flake8-executable</td>
<td>Checks shebang lines and file permissions. Too niche for most projects.</td>
</tr>
</tbody></table>
<hr />
<h2>Part 3: mypy for type checking</h2>
<p>Ruff catches style issues, bugs, and anti-patterns. But it doesn't understand types across your codebase. That's mypy's job.</p>
<h3>Why <code>strict</code> mode?</h3>
<p>mypy's <code>--strict</code> flag enables all strict type checking options at once. Without it, mypy allows large parts of your code to go unchecked — untyped functions are silently ignored.</p>
<pre><code class="language-toml">[tool.mypy]
strict = true
python_version = "3.13"
</code></pre>
<p>With <code>strict = true</code>, mypy enables all of the following flags:</p>
<ul>
<li><p><code>warn_unused_configs</code> — warn if a config section doesn't match any files</p>
</li>
<li><p><code>disallow_any_generics</code> — no bare <code>list</code>, must be <code>list[int]</code></p>
</li>
<li><p><code>disallow_subclassing_any</code> — forbid subclassing values of type <code>Any</code></p>
</li>
<li><p><code>disallow_untyped_calls</code> — forbid calling untyped functions from typed ones</p>
</li>
<li><p><code>disallow_untyped_defs</code> — every function must have type annotations</p>
</li>
<li><p><code>disallow_incomplete_defs</code> — forbid partially typed function definitions</p>
</li>
<li><p><code>check_untyped_defs</code> — type-check the body of untyped functions too</p>
</li>
<li><p><code>disallow_untyped_decorators</code> — forbid decorators without type annotations</p>
</li>
<li><p><code>warn_redundant_casts</code> — flag unnecessary <code>cast()</code> calls</p>
</li>
<li><p><code>warn_unused_ignores</code> — flag <code># type: ignore</code> comments that aren't needed</p>
</li>
<li><p><code>warn_return_any</code> — flag functions that return <code>Any</code></p>
</li>
<li><p><code>no_implicit_reexport</code> — imports must be explicit to be re-exported</p>
</li>
<li><p><code>strict_equality</code> — forbid comparisons between unrelated types</p>
</li>
<li><p><code>strict_bytes</code> — strict handling of <code>bytes</code> vs <code>str</code></p>
</li>
<li><p><code>extra_checks</code> — enable additional checks beyond the standard strict set</p>
</li>
</ul>
<p>You don't need to list these individually — <code>strict = true</code> covers them all.</p>
<h3>Additional options worth enabling</h3>
<p>Beyond <code>strict</code>, a few extra options catch more issues:</p>
<pre><code class="language-toml">[tool.mypy]
strict = true
python_version = "3.13"
namespace_packages = true
show_error_codes = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
</code></pre>
<ul>
<li><p><code>namespace_packages</code>: support for PEP 420 namespace packages (no <code>__init__.py</code> required).</p>
</li>
<li><p><code>show_error_codes</code>: show the error code in each message (e.g., <code>[assignment]</code>), making it easy to add targeted <code># type: ignore[code]</code> comments.</p>
</li>
<li><p><code>enable_error_code</code>: extra checks not included in <code>strict</code>:</p>
<ul>
<li><p><code>ignore-without-code</code> — flags <code># type: ignore</code> without a specific error code (pairs with Ruff's <code>PGH003</code>)</p>
</li>
<li><p><code>redundant-expr</code> — flags expressions that are always <code>True</code> or always <code>False</code></p>
</li>
<li><p><code>truthy-bool</code> — flags suspicious truth-value checks on non-boolean types</p>
</li>
</ul>
</li>
</ul>
<h3>What about mypy plugins?</h3>
<p>Only add plugins for libraries you actually use. Common ones:</p>
<pre><code class="language-toml">plugins = ["pydantic.mypy"]  # only if you use pydantic
</code></pre>
<p>Don't add plugins for libraries not in your dependencies — mypy will error on startup.</p>
<h3>How Ruff and mypy complement each other</h3>
<p>Some areas where they overlap and reinforce each other:</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Ruff</th>
<th>mypy</th>
</tr>
</thead>
<tbody><tr>
<td>Unused imports</td>
<td><code>F401</code> — removes them</td>
<td>Also flags them but can't auto-fix</td>
</tr>
<tr>
<td>Type annotation style</td>
<td><code>UP</code> — modernizes syntax</td>
<td>Understands both old and new syntax</td>
</tr>
<tr>
<td><code>type: ignore</code> comments</td>
<td><code>PGH003</code> — requires error code</td>
<td><code>ignore-without-code</code> — same check</td>
</tr>
<tr>
<td>Unreachable code</td>
<td><code>SIM</code> — simplifies dead branches</td>
<td>Detects based on type narrowing</td>
</tr>
<tr>
<td>Missing return types</td>
<td>Not checked</td>
<td><code>disallow_untyped_defs</code></td>
</tr>
<tr>
<td>Wrong argument types</td>
<td>Not checked</td>
<td>Core feature</td>
</tr>
<tr>
<td><code>None</code> safety</td>
<td>Not checked</td>
<td>Core feature</td>
</tr>
</tbody></table>
<p><strong>The key insight</strong>: Ruff catches problems that are visible in a single file. mypy catches problems that require understanding types across your entire project. You need both.</p>
<h3>Don't use <code>ANN</code> — use mypy instead</h3>
<p>Ruff's <code>ANN</code> (flake8-annotations) rule set checks for missing type annotations. But if you're using <code>mypy --strict</code>, mypy already enforces this with <code>disallow_untyped_defs</code>. Running both creates duplicate warnings for the same issue. Skip <code>ANN</code> and let mypy handle type annotations.</p>
<hr />
<h2>Two configuration options worth knowing</h2>
<h3><code>unfixable</code> — warn but don't auto-fix</h3>
<p>When you run <code>ruff check --fix</code>, Ruff auto-fixes everything it can. But sometimes you want a rule to <strong>warn</strong> without auto-removing code. That's what <code>unfixable</code> is for.</p>
<p>A common example is <code>ERA</code> (flake8-eradicate), which detects commented-out code. If you enable it with <code>--fix</code>, Ruff will silently delete those comments. If you'd rather review them manually:</p>
<pre><code class="language-toml">[tool.ruff.lint]
extend-select = ["ERA"]
unfixable = ["ERA"]  # warn about commented-out code, but don't auto-delete it
</code></pre>
<p>This way <code>ruff check</code> shows the warnings, but <code>ruff check --fix</code> leaves them untouched.</p>
<p>A common mistake is combining this with <code>ignore = ["ERA001"]</code> — that silences the only rule in <code>ERA</code>, making the entire setup do nothing.</p>
<h3><code>known-first-party</code> — help isort identify your packages</h3>
<p>isort needs to know which imports belong to your project (first-party) vs third-party packages. Ruff usually detects this automatically from <code>pyproject.toml</code>, but in monorepos or when your package name differs from the project name, it can get confused.</p>
<pre><code class="language-toml">[tool.ruff.lint.isort]
known-first-party = ["myapp"]
</code></pre>
<p>Without this, isort might sort your project's imports into the third-party group, creating unnecessary blank lines and wrong ordering:</p>
<pre><code class="language-python"># Wrong — isort thinks 'myapp' is third-party
import requests
import myapp  # grouped with third-party

# Correct — isort knows 'myapp' is first-party
import requests

import myapp  # correctly separated as local
</code></pre>
<p>You only need this when isort misclassifies your imports. If everything looks correct without it, don't add it.</p>
<h3><code>required-imports</code> — auto-inject imports into every file</h3>
<p>isort can force a specific import to be present in every file. A common use case:</p>
<pre><code class="language-toml">[tool.ruff.lint.isort]
required-imports = ["from __future__ import annotations"]
</code></pre>
<p>This makes <code>from __future__ import annotations</code> appear at the top of every file. That import changes how Python evaluates type annotations — they become strings instead of being evaluated at runtime (PEP 563), which allows forward references and speeds up import time.</p>
<p><strong>Should you use it?</strong> It depends on your Python version:</p>
<ul>
<li><p><strong>Python &lt; 3.10</strong>: useful. It lets you write <code>list[int]</code> and <code>X | Y</code> in annotations instead of <code>List[int]</code> and <code>Union[X, Y]</code>.</p>
</li>
<li><p><strong>Python 3.10+</strong>: unnecessary. Modern type syntax (<code>X | Y</code>, <code>list[int]</code>) works natively without the future import.</p>
</li>
</ul>
<p>If your <code>target-version</code> is <code>py310</code> or higher, skip this option — it adds a line of noise to every file for no benefit.</p>
<hr />
<h2>The complete <code>pyproject.toml</code></h2>
<p>Here's everything together — formatter, linter, and type checker — in a single file:</p>
<pre><code class="language-toml">[tool.ruff]
target-version = "py313"
line-length = 88

[tool.ruff.lint]
extend-select = [
    "A",     # flake8-builtins
    "ARG",   # flake8-unused-arguments
    "ASYNC", # flake8-async
    "B",     # flake8-bugbear
    "BLE",   # flake8-blind-except
    "C4",    # flake8-comprehensions
    "C90",   # mccabe
    "DTZ",   # flake8-datetimez
    "E",     # pycodestyle errors (full set)
    "EM",    # flake8-errmsg
    "FBT",   # flake8-boolean-trap
    "FLY",   # flynt
    "FURB",  # refurb
    "G",     # flake8-logging-format
    "I",     # isort
    "ICN",   # flake8-import-conventions
    "ISC",   # flake8-implicit-str-concat
    "INT",   # flake8-gettext
    "LOG",   # flake8-logging
    "N",     # pep8-naming
    "PERF",  # perflint
    "PGH",   # pygrep-hooks
    "PIE",   # flake8-pie
    "PL",    # pylint
    "PTH",   # flake8-use-pathlib
    "Q",     # flake8-quotes
    "RET",   # flake8-return
    "RSE",   # flake8-raise
    "RUF",   # ruff-specific rules
    "S",     # flake8-bandit
    "SIM",   # flake8-simplify
    "SLF",   # flake8-self
    "SLOT",  # flake8-slots
    "T10",   # flake8-debugger
    "T20",   # flake8-print
    "TC",    # flake8-type-checking
    "TID",   # flake8-tidy-imports
    "TRY",   # tryceratops
    "UP",    # pyupgrade
    "W",     # pycodestyle warnings
    "YTT",   # flake8-2020
]
ignore = [
    "PLR0913",  # too many arguments — sometimes unavoidable
    "PLR2004",  # magic value comparison — too many false positives
    "TRY003",   # long exception messages — sometimes clearer inline
]

[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"

[tool.ruff.lint.isort]
force-single-line = true
lines-between-types = 1
lines-after-imports = 2
# known-first-party = ["myapp"]  # uncomment if isort misclassifies your imports

[tool.mypy]
strict = true
python_version = "3.13"
namespace_packages = true
show_error_codes = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
</code></pre>
<h3>Running everything</h3>
<pre><code class="language-bash">ruff format .          # format code
ruff check . --fix     # lint and auto-fix what's possible
mypy .                 # type check
</code></pre>
<p>In CI, use the check-only variants:</p>
<pre><code class="language-bash">ruff format --check .  # fail if formatting needed
ruff check .           # fail on lint errors
mypy .                 # fail on type errors
</code></pre>
<hr />
<h2>VS Code: format and lint on save</h2>
<p>You can make VS Code run <code>ruff format</code> and <code>ruff check --fix</code> every time you save a file, so you never have to think about it.</p>
<h3>Required extensions</h3>
<p>Install these two VS Code extensions:</p>
<ul>
<li><p><a href="https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff"><strong>Ruff</strong></a> (<code>charliermarsh.ruff</code>) — linting and formatting</p>
</li>
<li><p><a href="https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker"><strong>Mypy Type Checker</strong></a> (<code>ms-python.mypy-type-checker</code>) — type checking</p>
</li>
</ul>
<h3>Configuration</h3>
<p>Add this to your <code>.vscode/settings.json</code>:</p>
<pre><code class="language-json">{
    // Ruff as the default formatter for Python
    "[python]": {
        "editor.defaultFormatter": "charliermarsh.ruff",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
            "source.fixAll.ruff": "explicit",
            "source.organizeImports.ruff": "explicit"
        }
    },

    // Disable the built-in Python formatter to avoid conflicts
    "python.formatting.provider": "none",

    // Use the project's installed binaries instead of the bundled ones
    "ruff.importStrategy": "fromEnvironment",
    "mypy-type-checker.importStrategy": "fromEnvironment"
}
</code></pre>
<h3>What happens on every save</h3>
<ol>
<li><p><code>editor.formatOnSave</code> — runs <code>ruff format</code> on the file (fixes quotes, indentation, trailing commas, line length)</p>
</li>
<li><p><code>source.fixAll.ruff</code> — runs <code>ruff check --fix</code> (auto-fixes lint issues like unused imports, unsorted imports, modernizable syntax)</p>
</li>
<li><p><code>source.organizeImports.ruff</code> — sorts imports via the <code>I</code> rule (stdlib → third-party → local)</p>
</li>
<li><p><strong>mypy</strong> — runs in the background and shows type errors as squiggly underlines in the editor</p>
</li>
</ol>
<h3>Why <code>"fromEnvironment"</code> instead of the bundled binary?</h3>
<p>Both extensions ship with a bundled version of their tool. By default, they use that bundled binary — which may be a different version than the one installed in your project's virtual environment.</p>
<p>Setting <code>"importStrategy": "fromEnvironment"</code> tells the extension to use the binary from your project's environment (e.g., <code>.venv/bin/ruff</code>). This ensures that VS Code uses <strong>the exact same version</strong> as your CI and your teammates, avoiding "works on my machine" issues where the bundled version has different rules or behavior.</p>
<h3>Why <code>"explicit"</code> instead of <code>"always"</code>?</h3>
<p>Using <code>"explicit"</code> means these code actions run on save but <strong>only when you explicitly save</strong> (Cmd+S). With <code>"always"</code>, they would also run on auto-save and window focus changes, which can be disruptive when you're in the middle of typing.</p>
<h3>Project-level vs user-level settings</h3>
<p>Put this in <code>.vscode/settings.json</code> at the root of your project (not in your global user settings). This way every developer on the team gets the same behavior without having to configure anything. Add the <code>.vscode/</code> folder to your git repo.</p>
<hr />
<h2>Final thoughts</h2>
<p>The Python tooling landscape has consolidated. In 2026, you don't need a dozen tools — you need two:</p>
<ul>
<li><p><strong>Ruff</strong> for formatting and linting (replaces Black, isort, flake8 + plugins, pylint, pyupgrade)</p>
</li>
<li><p><strong>mypy</strong> for type checking (nothing else does this as well)</p>
</li>
</ul>
<p>Start with the full configuration above. If a specific rule is too noisy for your codebase, silence it with <code>ignore</code> — it's always better to start strict and relax than to start loose and tighten later.</p>
]]></content:encoded></item><item><title><![CDATA[Perfect Python Environment (Poetry)]]></title><description><![CDATA[Greetings! This post shows step by step the installation and configuration of all the tools needed to set up an agile and robust Python development environment. It is assumed that the text editor to be used will be VSCode and that the environment wil...]]></description><link>https://blog.marcosalonso.dev/perfect-python-environment-poetry</link><guid isPermaLink="true">https://blog.marcosalonso.dev/perfect-python-environment-poetry</guid><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[Poetry]]></category><category><![CDATA[ruff]]></category><category><![CDATA[Mypy]]></category><category><![CDATA[best practices]]></category><category><![CDATA[Python Env]]></category><dc:creator><![CDATA[Marcos Alonso]]></dc:creator><pubDate>Sun, 14 Dec 2025 19:58:34 GMT</pubDate><content:encoded><![CDATA[<p>Greetings! This post shows step by step the installation and configuration of all the tools needed to set up an agile and robust Python development environment. It is assumed that the text editor to be used will be VSCode and that the environment will be running on a Debian/Ubuntu based Linux system (including WSL).</p>
<h2 id="heading-managing-multiple-python-versions">Managing multiple Python versions</h2>
<p>Not all projects will always be developed on the same version of Python. Sometimes we will need Python 3.9, sometimes Python 3.12... That is why it is very important to be able to move between versions and migrate them easily.</p>
<p>This is where <a target="_blank" href="https://github.com/pyenv/pyenv?ref=blog.marcosalonso.dev"><strong>PyEnv</strong></a> comes into play, a tool that allows precisely this: to install and switch from one Python version to another with ease. For its installation we can use their official installer:</p>
<pre><code class="lang-bash">curl https://pyenv.run | bash
</code></pre>
<p>Once we have it installed, it is necessary to add the pertinent variables to our Shell, something that can be done in a simple way following <a target="_blank" href="https://github.com/pyenv/pyenv?ref=blog.marcosalonso.dev#set-up-your-shell-environment-for-pyenv">its official documentation</a>.</p>
<p>Finally, it is necessary to install a Python version of our choice and set it as the global default version:</p>
<pre><code class="lang-bash">pyenv install 3.8.10
pyenv global 3.8.10
</code></pre>
<p>This concludes the installation of PyEnv. Whenever we need a new version of Python on our system, we can install it with the command <strong>pyenv install</strong>.</p>
<h2 id="heading-installing-global-and-isolated-python-packages">Installing global and isolated Python packages</h2>
<p>To install some of the tools needed in this environment, it will be necessary to have Python applications that run globally. To avoid any conflicts, the best way to install these tools is <strong>in isolation</strong>. For this purpose, the <strong>pipx</strong> tool that perfectly fulfills this task.</p>
<p>Installation is quite simple:</p>
<ul>
<li>For Ubuntu 23.04 or above:</li>
</ul>
<pre><code class="lang-bash">sudo apt update
sudo apt install pipx
pipx ensurepath
</code></pre>
<ul>
<li>For Ubuntu 22.04 or below <em>(you need to have a global Python versions installed with PyEnv!)</em>:</li>
</ul>
<pre><code class="lang-bash">python3 -m pip install --user pipx
python3 -m pipx ensurepath
</code></pre>
<h2 id="heading-installing-and-using-poetry-our-project-manager">Installing and using Poetry, our project manager</h2>
<h3 id="heading-installation">Installation</h3>
<p>Since we have installed pipx, the installation of <a target="_blank" href="https://github.com/python-poetry/poetry?ref=blog.marcosalonso.dev"><strong>Poetry</strong></a> is very simple:</p>
<pre><code class="lang-bash">pipx install poetry
</code></pre>
<p>After installing Poetry, we need to configure it to be able to read the current version of Python from PyEnv. Just run the above command:</p>
<pre><code class="lang-bash">poetry config virtualenvs.prefer-active-python <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-reading-dotenv-file">Reading dotenv file</h3>
<p>Poetry does not include native support for reading .env files and loading environment variables, something that other tools such as <a target="_blank" href="https://github.com/pypa/pipenv?ref=blog.marcosalonso.dev">Pipenv</a> do. To solve this, there is the <a target="_blank" href="https://github.com/mpeteuil/poetry-dotenv-plugin?ref=blog.marcosalonso.dev"><strong>poetry-dotenv-plugin</strong></a>. Its installation in our Poetry environment is simple:</p>
<pre><code class="lang-bash">poetry self add poetry-dotenv-plugin
</code></pre>
<h3 id="heading-creating-a-poetry-project">Creating a Poetry project</h3>
<p>To start our Poetry project, we must first make sure that we have the desired Python version installed in PyEnv, switch to it and start the new project:</p>
<pre><code class="lang-plaintext">pyenv install 3.12.0
pyenv local 3.12.0

poetry new your-project
cd your-project
poetry install
</code></pre>
<p>With this, we will have the initial structure of a project with the desired Python version. We can check the environment being used with the following command:</p>
<pre><code class="lang-bash">poetry env info
</code></pre>
<p>This should return an output similar to:</p>
<pre><code class="lang-plaintext">Virtualenv
Python:         3.12.0
Implementation: CPython
Path:           /home/marcos/.cache/pypoetry/virtualenvs/test-F5kZevV3-py3.12
Executable:     /home/marcos/.cache/pypoetry/virtualenvs/test-F5kZevV3-py3.12/bin/python
Valid:          True

System
Platform:   linux
OS:         posix
Python:     3.12.0
Path:       /home/marcos/.pyenv/versions/3.12.0
Executable: /home/marcos/.pyenv/versions/3.12.0/bin/python3.12
</code></pre>
<h2 id="heading-linting-and-formatting-your-code">Linting and formatting your code</h2>
<p>Once we have the initial structure of the project, we should not start programming until we are sure that our code will be coherent, readable and follow the official style guides.</p>
<p>To achieve this, I have chosen to use <a target="_blank" href="https://github.com/astral-sh/ruff?ref=blog.marcosalonso.dev"><strong>ruff</strong></a>. This tool is both a linter and a formatter, being extremely fast because it is programmed in <a target="_blank" href="https://www.rust-lang.org/?ref=blog.marcosalonso.dev">Rust</a>.</p>
<p>This tool must be installed in each of the projects in which we want to use it, as a development dependency:</p>
<pre><code class="lang-bash">poetry add ruff -D
</code></pre>
<p>The Ruff settings I like to set on the <strong>pyproject.toml</strong> are the following:</p>
<pre><code class="lang-ini"><span class="hljs-section">[tool.ruff]</span>
<span class="hljs-attr">fix</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">line-length</span> = <span class="hljs-number">88</span>

<span class="hljs-section">[tool.ruff.lint]</span>
<span class="hljs-attr">unfixable</span> = [
    <span class="hljs-string">"ERA"</span>, <span class="hljs-comment"># do not autoremove commented out code</span>
]
<span class="hljs-attr">extend-select</span> = [
    <span class="hljs-string">"B"</span>,   <span class="hljs-comment"># flake8-bugbear</span>
    <span class="hljs-string">"C4"</span>,  <span class="hljs-comment"># flake8-comprehensions</span>
    <span class="hljs-string">"ERA"</span>, <span class="hljs-comment"># flake8-eradicate/eradicate</span>
    <span class="hljs-string">"I"</span>,   <span class="hljs-comment"># isort</span>
    <span class="hljs-string">"N"</span>,   <span class="hljs-comment"># pep8-naming</span>
    <span class="hljs-string">"PIE"</span>, <span class="hljs-comment"># flake8-pie</span>
    <span class="hljs-string">"PGH"</span>, <span class="hljs-comment"># pygrep</span>
    <span class="hljs-string">"RUF"</span>, <span class="hljs-comment"># ruff checks</span>
    <span class="hljs-string">"SIM"</span>, <span class="hljs-comment"># flake8-simplify</span>
    <span class="hljs-string">"TCH"</span>, <span class="hljs-comment"># flake8-type-checking</span>
    <span class="hljs-string">"TID"</span>, <span class="hljs-comment"># flake8-tidy-imports</span>
    <span class="hljs-string">"UP"</span>,  <span class="hljs-comment"># pyupgrade</span>
]
<span class="hljs-attr">ignore</span> = [
    <span class="hljs-string">"ERA001"</span>
]

<span class="hljs-section">[tool.ruff.lint.flake8-tidy-imports]</span>
<span class="hljs-attr">ban-relative-imports</span> = <span class="hljs-string">"all"</span>

<span class="hljs-section">[tool.ruff.lint.isort]</span>
<span class="hljs-attr">force-single-line</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">lines-between-types</span> = <span class="hljs-number">1</span>
<span class="hljs-attr">lines-after-imports</span> = <span class="hljs-number">2</span>
<span class="hljs-attr">known-first-party</span> = [<span class="hljs-string">"your_package"</span>]
<span class="hljs-attr">required-imports</span> = [<span class="hljs-string">"from __future__ import annotations"</span>]
</code></pre>
<h4 id="heading-vscode-with-ruff">VSCode with ruff</h4>
<p>To set ruff as our formatter in VSCode and not have to use its command line, we must:</p>
<ol>
<li><p>Download <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff&amp;ref=blog.marcosalonso.dev">the Astral Software ruff extension</a> from the VSCode store.</p>
</li>
<li><p>Set ruff as our default Python formatter. This can be done at the user or workspace configuration level (.vscode/settings.json):</p>
</li>
</ol>
<pre><code class="lang-json">{
    <span class="hljs-attr">"[python]"</span>: {
        <span class="hljs-attr">"editor.codeActionsOnSave"</span>: {
            <span class="hljs-attr">"source.organizeImports"</span>: <span class="hljs-string">"explicit"</span>,
            <span class="hljs-attr">"source.fixAll"</span>: <span class="hljs-string">"explicit"</span>,
        },
        <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"charliermarsh.ruff"</span>,
    }
}
</code></pre>
<ol start="3">
<li><p>Activate the "editor.formatOnSave" option so that the current file is automatically formatted every time we save.</p>
</li>
<li><p>In the extension configuration, we must select the option to use the Ruff binary from environment.</p>
</li>
</ol>
<p>This way, we will have both the formatter and linter from ruff running in our workspace, with perfect integration with VSCode. If you prefer to use another linter/formatter, ruff allows you to disable these features from the pyproject.toml file.</p>
<h2 id="heading-ensuring-consistent-typing">Ensuring consistent typing</h2>
<p>When programming, in order to ensure type consistency throughout the entire code flow and to avoid errors, it is very useful to use strong typing. This is especially important in languages such as Python, which by default are very flexible in this respect.</p>
<p>To obtain this typing consistency, we will use the mypy tool. Installing it is as easy as installing ruff, and we will have to do it in each of the projects in which we want to use it:</p>
<pre><code class="lang-bash">poetry add mypy -D
</code></pre>
<p>Once installed, we will activate its strict configuration. This configuration, although it may cause more errors and take more time to correct them, is the most robust. To activate it, we must add the following section to the pyproject.toml file:</p>
<pre><code class="lang-toml"><span class="hljs-section">[tool.mypy]</span>
<span class="hljs-attr">namespace_packages</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">show_error_codes</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">enable_error_code</span> = [
    <span class="hljs-string">"ignore-without-code"</span>,
    <span class="hljs-string">"redundant-expr"</span>,
    <span class="hljs-string">"truthy-bool"</span>,
]
<span class="hljs-attr">strict</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">files</span> = [<span class="hljs-string">"your_package"</span>, <span class="hljs-string">"tests"</span>]
</code></pre>
<h5 id="heading-vscode-with-mypy">VSCode with mypy</h5>
<p>To integrate mypy into VSCode and receive this typing error report in the interface, we must follow the steps below:</p>
<ol>
<li><p>Install <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker">the Microsoft Mypy Type Checker extension</a> from the VSCode store.</p>
</li>
<li><p>In the extension configuration, we must select the option to use the MyPy binary from environment.</p>
</li>
</ol>
<h2 id="heading-final-thoughts">Final thoughts</h2>
<p>Following these steps, we get what for me is being the Python programming environment that I am enjoying using the most and that has generated more efficiency when working on my projects.</p>
<p>As always, everyone has their own tools and methodologies, and any of the components described here are open to improvement.</p>
<p>If you want to further improve the way you do your Python projects, I recommend that you learn more about <strong>the pyproject.toml file</strong>, for example through <a target="_blank" href="https://packaging.python.org/en/latest/guides/writing-pyproject-toml/?ref=blog.marcosalonso.dev">this article</a>.</p>
]]></content:encoded></item></channel></rss>