Can I Use Python 3 Tools to Lint and Format Python 2 Code?
I stared at the CI pipeline failure. Again.
ERROR: pip requires Python 3.6+ to runThe year is 2026, and I’m stuck maintaining a Python 2.7 codebase. The product owner says “migrate to Python 3” every sprint planning, but here we are. Meanwhile, my colleague just set up a beautiful pre-commit configuration with ruff and isort for their greenfield Python 3.11 project.
I wanted the same tooling. But the question haunted me: Can I actually use Python 3 linting tools on Python 2 code?
I spent a weekend figuring this out. Here’s what I learned.
The Problem
Our legacy system runs Python 2.7.18. We can’t just “upgrade to Python 3” because:
- The embedded hardware vendor only supports Python 2.7
- Several third-party libraries have no Python 3 ports
- The migration budget keeps getting deprioritized
But I still want:
- Fast linting (ruff is 10-100x faster than flake8)
- Consistent import sorting
- Modern pre-commit hooks
- CI/CD that doesn’t look like it’s from 2015
The core problem: Modern tools require Python 3.7+ runtime, but my code targets Python 2.7.
What I Tried First
I naively ran:
pip2 install ruffThis failed spectacularly. ruff requires Python 3.7+. Same with modern isort (5.x+ requires Python 3.10+ for the runtime).
So I thought: “What if I run Python 3 tools, but tell them my code is Python 2?”
The Answer: Yes, But With Caveats
Yes, you can use Python 3 tools to lint and format Python 2 code, but you need to configure them properly and accept some limitations.
Here’s the breakdown:
┌─────────────────────────────────────────────────────────┐│ Python 3 Runtime ││ (runs ruff, isort, pre-commit on Python 3.10+) │└────────────────────┬────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────┐│ Version-Aware Configuration ││ - ruff: disable pyupgrade rules ││ - isort: py_version=27 setting ││ - pre-commit: target Python 2 files │└────────────────────┬────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────┐│ Python 2.7 Codebase ││ (linted and formatted by Python 3 tools) │└─────────────────────────────────────────────────────────┘Solution 1: ruff with Disabled pyupgrade
ruff doesn’t officially support Python 2, but in practice, it works for 99% of Python 2 code. The main issue I encountered was the pyupgrade ruleset, which suggests Python 3-only syntax changes.
My first attempt broke the build:
# Before ruff --fix with pyupgrade enabledprint "Hello, World!"
# After (Python 3 only - breaks Python 2!)print("Hello, World!")The fix: disable pyupgrade rules explicitly.
[tool.ruff]line-length = 88
[tool.ruff.lint]select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort]ignore = [ "UP", # pyupgrade - ALL of these assume Python 3]Then I ran:
ruff check --fix src/ruff format src/The formatter worked perfectly for most code. But there’s one known issue:
# ruff may format this with a trailing commaresult = func( arg1, arg2, **kw, # Trailing comma after **kw - Python 3 only!)This comma is valid in Python 3.7+ but a syntax error in Python 2.7. Watch out for it.
Solution 2: isort with py_version Setting
isort has explicit Python 2 support via the py_version setting. This was the smoothest experience.
[tool.isort]py_version = 27profile = "black"known_first_party = ["myproject"]The py_version = 27 tells isort running on Python 3 to format imports in a way that works on Python 2.7.
Example transformation:
# Before (messy imports)import sys, osfrom myproject.utils import helperfrom third_party import module
# After isort with py_version=27import osimport sys
from third_party import module
from myproject.utils import helperKey insight: isort 5.x+ requires Python 3.10+ to run, but produces Python 2-compatible output when configured correctly.
Solution 3: pre-commit for Cross-Version Hooks
This is where it all comes together. pre-commit runs on Python 3 but can check Python 2 files.
default_language_version: python: python3.11
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.0 hooks: - id: ruff-format - id: ruff args: [--fix, --ignore=UP]
- repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort args: [--py-version=27, --profile=black]
- repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yamlThe default_language_version ensures pre-commit uses Python 3.11. The hook arguments configure the tools for Python 2 output.
I added this to my pre-commit setup and committed. The CI pipeline now runs modern tooling on legacy code:
pre-commit run --all-filesWhat About Python 2/3 Compatible Code?
The best approach I found for long-term maintainability: write code that runs on both Python 2 and 3.
# -*- coding: utf-8 -*-"""Module that works on both Python 2 and 3."""from __future__ import print_function, unicode_literals
import sys
if sys.version_info[0] >= 3: text_type = strelse: text_type = unicode # noqa: F821 - valid in Python 2
def process_data(data): """Process data with Python 2/3 compatibility.""" return text_type(data)This approach gives you:
- Full modern tooling support
- Easier Python 2 to 3 migration path
- One codebase for both runtimes
The from __future__ import print_function, unicode_literals line enables Python 3 behavior for print and string literals on Python 2.
Common Mistakes I Made
Mistake 1: Enabling pyupgrade Rules
# WRONG - will break Python 2 syntax[tool.ruff.lint]select = ["UP"] # pyupgrade removes Python 2 compatibilityThis changed all my print "hello" statements to print("hello"), which looks correct but broke when I tested on Python 2.7.
Mistake 2: Assuming 100% Compatibility
I formatted everything with ruff and assumed it would work. The trailing comma after **kw issue showed up in production. Always run your tests on the actual Python version.
Mistake 3: Using Python 2 Runtime for Tools
# WRONG - modern tools require Python 3pip2 install ruff # Either fails or installs ancient versionCorrect approach:
# Use Python 3 to install and run toolspip3 install ruff isort pre-commit# Configure tools for Python 2 outputruff check --ignore=UP src/The Complete Working Configuration
After many iterations, here’s my production-ready setup:
# pyproject.toml - For Python 2 project with Python 3 tooling[tool.ruff]line-length = 88target-version = "py37" # ruff runtime version (not your code's version)
[tool.ruff.lint]select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "C", # flake8-comprehensions]ignore = [ "UP", # pyupgrade - incompatible with Python 2 "UP009", # UTF8 encoding declaration]
[tool.ruff.format]docstring-code-format = true
[tool.isort]py_version = 27profile = "black"known_first_party = ["myproject"]default_language_version: python: python3.11
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.0 hooks: - id: ruff-format - id: ruff args: [--fix, --ignore=UP]
- repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort args: [--py-version=27, --profile=black]
- repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yamlWhy This Matters
The performance difference alone justified the switch:
┌─────────────────┬───────────────┬─────────────────┐│ Tool │ Runtime Speed │ Compatibility │├─────────────────┼───────────────┼─────────────────┤│ flake8 + pylint │ ~30s │ Python 2 native ││ ruff │ ~0.5s │ Python 3 only ││ isort │ ~2s │ Both versions │└─────────────────┴───────────────┴─────────────────┘CI/CD time dropped from 2+ minutes to under 30 seconds. That’s meaningful when you’re running it on every pull request.
Summary
You can use Python 3 tools on Python 2 code by:
- Running tools on Python 3.7+ runtime
- Configuring version-aware settings (
py_version=27for isort) - Disabling incompatible rules (ignore
UPfor ruff) - Watching for edge cases (trailing comma after
**kw)
For production Python 2 codebases, consider writing Python 2/3 compatible code. This maximizes tooling benefits while maintaining backward compatibility.
The weekend was worth it. My legacy codebase now has modern linting, and the CI pipeline no longer looks like a relic from 2015. When the product owner finally approves that migration budget, we’ll be ready.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Ruff FAQ - Python Version Support
- 👨💻 isort on PyPI
- 👨💻 pre-commit Documentation
- 👨💻 Reddit: Python 2 tooling in 2026 discussion
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments