Skip to main content

Python Linting for FastAPI: Pylint vs Flake8 vs Mypy

This comprehensive guide compares three popular Python linting tools (Pylint, Flake8, and Mypy) in the context of building a FastAPI backend project. Learn when to use each tool and how to configure them for optimal code quality.

Overview

FeaturePylintFlake8Mypy
Primary FocusCode quality & standardsStyle & complexityType checking
Type Checking❌ No❌ No✅ Yes
CustomizationVery HighHighMedium
PerformanceSlowFastMedium
ConfigurationComplexSimpleMedium
Error DetectionComprehensiveBasic to MediumType-related only
IDE IntegrationExcellentExcellentExcellent
FastAPI Support✅ Good✅ Good✅ Excellent

1. Introduction to Linting

Linting is the automated checking of source code for programmatic and stylistic errors. It helps maintain code quality, consistency, and can catch bugs before runtime.

Why Use Multiple Linters?

Each linter serves a different purpose:

  • Pylint: Comprehensive code quality checks
  • Flake8: Code style and complexity
  • Mypy: Static type checking

Using all three together provides complete coverage of code quality, style, and type safety.

2. Installation and Basic Setup

Installing the Linters

# Install all three linters
pip install pylint flake8 mypy

# Or add to requirements-dev.txt
echo "pylint==3.0.0" >> requirements-dev.txt
echo "flake8==6.1.0" >> requirements-dev.txt
echo "mypy==1.7.0" >> requirements-dev.txt
pip install -r requirements-dev.txt

Basic FastAPI Project Structure

fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── schemas.py
│ └── routes/
│ ├── __init__.py
│ └── users.py
├── tests/
│ └── test_main.py
├── .pylintrc
├── .flake8
├── mypy.ini
└── requirements.txt

3. Pylint - Comprehensive Code Quality

What Pylint Checks

Pylint is a comprehensive linter that checks for:

  • Code errors and potential bugs
  • Code smells and bad practices
  • PEP 8 style violations
  • Code complexity
  • Naming conventions
  • Documentation issues

Example FastAPI Code

# app/main.py
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List
from . import models, schemas
from .database import get_db

app = FastAPI(title="User API", version="1.0.0")

@app.get("/users/", response_model=List[schemas.User])
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""
Retrieve a list of users.

Args:
skip: Number of records to skip
limit: Maximum number of records to return
db: Database session

Returns:
List of users
"""
users = db.query(models.User).offset(skip).limit(limit).all()
return users

@app.post("/users/", response_model=schemas.User)
async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
"""Create a new user."""
db_user = models.User(**user.dict())
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user

Running Pylint

# Run Pylint on a file
pylint app/main.py

# Run on entire directory
pylint app/

# Get score
pylint app/ --score=yes

# Output to file
pylint app/ > pylint_report.txt

Pylint Configuration (.pylintrc)

[MASTER]
# Use multiple processes to speed up Pylint
jobs=4

# Add files or directories to the blacklist
ignore=migrations,venv,.venv,build,dist

[MESSAGES CONTROL]
# Disable specific warnings
disable=
C0111, # missing-docstring
C0103, # invalid-name
R0903, # too-few-public-methods (common in FastAPI schemas)
R0913, # too-many-arguments (common in FastAPI endpoints)
W0212, # protected-access (sometimes needed)

[FORMAT]
# Maximum line length
max-line-length=120

# Maximum number of characters on a single line
single-line-if-stmt=no

[DESIGN]
# Maximum number of arguments for function/method
max-args=8

# Maximum number of attributes for a class
max-attributes=10

[BASIC]
# Good variable names
good-names=i,j,k,db,id,pk,_

# Naming style
variable-rgx=[a-z_][a-z0-9_]{2,30}$
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$

Common Pylint Issues in FastAPI

# ❌ Bad: Too many arguments (R0913)
@app.post("/process/")
async def process_data(
param1: str, param2: str, param3: str,
param4: str, param5: str, param6: str,
param7: str, param8: str, param9: str
):
pass

# ✅ Good: Use Pydantic model
from pydantic import BaseModel

class ProcessRequest(BaseModel):
param1: str
param2: str
param3: str
# ... more fields

@app.post("/process/")
async def process_data(request: ProcessRequest):
pass

# ❌ Bad: Missing docstring (C0111)
@app.get("/items/")
async def get_items():
return []

# ✅ Good: With docstring
@app.get("/items/")
async def get_items():
"""Retrieve all items from the database."""
return []

# ❌ Bad: Variable name too short (C0103)
@app.get("/calculate/")
async def calculate(x: int, y: int):
r = x + y
return r

# ✅ Good: Descriptive variable names
@app.get("/calculate/")
async def calculate(x: int, y: int):
result = x + y
return result

Pylint Pros and Cons

Pros:

  • ✅ Most comprehensive linter
  • ✅ Catches subtle bugs and code smells
  • ✅ Highly configurable
  • ✅ Provides detailed reports with scores

Cons:

  • ❌ Slower than other linters
  • ❌ Can be overly strict by default
  • ❌ Configuration can be complex
  • ❌ May have false positives

4. Flake8 - Style and Complexity

What Flake8 Checks

Flake8 is a wrapper around three tools:

  • PyFlakes: Logical errors
  • pycodestyle: PEP 8 style violations
  • McCabe: Code complexity

Example FastAPI Code with Flake8

# app/models.py
from sqlalchemy import Column, Integer, String, Float, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
"""User model for database."""
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
full_name = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)

def __repr__(self):
return f"<User(username={self.username}, email={self.email})>"

Running Flake8

# Run Flake8 on a file
flake8 app/main.py

# Run on entire directory
flake8 app/

# Show statistics
flake8 app/ --statistics

# Check specific error codes
flake8 app/ --select=E,W

# Ignore specific error codes
flake8 app/ --ignore=E501,W503

Flake8 Configuration (.flake8)

[flake8]
# Maximum line length
max-line-length = 120

# Exclude directories
exclude =
.git,
__pycache__,
.venv,
venv,
build,
dist,
migrations,
*.egg-info

# Ignore specific errors
ignore =
E203, # whitespace before ':'
E266, # too many leading '#' for block comment
E501, # line too long (handled by max-line-length)
W503, # line break before binary operator
W504, # line break after binary operator

# Maximum complexity
max-complexity = 10

# Show source code for each error
show-source = True

# Show the relevant text for each error
show-pep8 = True

# Count the number of occurrences of each error/warning code
count = True

# Print total number of errors
statistics = True

Common Flake8 Issues in FastAPI

# ❌ Bad: Line too long (E501)
@app.post("/users/")
async def create_user(username: str, email: str, full_name: str, age: int, address: str, phone: str):
return {"username": username, "email": email, "full_name": full_name, "age": age}

# ✅ Good: Break into multiple lines
@app.post("/users/")
async def create_user(
username: str,
email: str,
full_name: str,
age: int,
address: str,
phone: str
):
return {
"username": username,
"email": email,
"full_name": full_name,
"age": age
}

# ❌ Bad: Unused import (F401)
from fastapi import FastAPI, HTTPException, Depends, Query
from typing import List, Optional, Dict

# Only using FastAPI and List

# ✅ Good: Remove unused imports
from fastapi import FastAPI
from typing import List

# ❌ Bad: Multiple imports on one line (E401)
from fastapi import FastAPI, HTTPException, Depends

# ✅ Good: Separate imports (optional, based on style guide)
from fastapi import FastAPI
from fastapi import HTTPException
from fastapi import Depends

# Or group them properly (also acceptable)
from fastapi import (
FastAPI,
HTTPException,
Depends,
)

# ❌ Bad: High complexity (C901)
@app.get("/status/")
async def check_status(value: int):
if value > 100:
if value > 200:
if value > 300:
return "very high"
return "high"
return "medium"
return "low"

# ✅ Good: Simplified logic
@app.get("/status/")
async def check_status(value: int):
if value > 300:
return "very high"
if value > 200:
return "high"
if value > 100:
return "medium"
return "low"

# Or use a lookup
@app.get("/status/")
async def check_status(value: int):
thresholds = [(300, "very high"), (200, "high"), (100, "medium")]
for threshold, status in thresholds:
if value > threshold:
return status
return "low"

Flake8 Plugins for FastAPI

# Install useful plugins
pip install flake8-docstrings # Check docstrings
pip install flake8-bugbear # Find likely bugs
pip install flake8-import-order # Check import order
pip install flake8-annotations # Check type annotations

Flake8 Pros and Cons

Pros:

  • ✅ Fast and lightweight
  • ✅ Easy to configure
  • ✅ Focuses on PEP 8 compliance
  • ✅ Great plugin ecosystem
  • ✅ Works well with CI/CD

Cons:

  • ❌ Less comprehensive than Pylint
  • ❌ Doesn't catch as many code smells
  • ❌ No type checking
  • ❌ Limited refactoring suggestions

5. Mypy - Static Type Checking

What Mypy Checks

Mypy is a static type checker for Python that verifies:

  • Type annotations correctness
  • Type consistency across the codebase
  • Potential runtime type errors
  • Function signature compliance

Why Mypy is Perfect for FastAPI

FastAPI heavily uses Python type hints, making Mypy essential for:

  • Ensuring Pydantic model type safety
  • Validating dependency injection types
  • Checking async function signatures
  • Preventing type-related bugs

Example FastAPI Code with Type Hints

# app/schemas.py
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

class UserBase(BaseModel):
"""Base user schema."""
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
full_name: Optional[str] = None

class UserCreate(UserBase):
"""Schema for creating a user."""
password: str = Field(..., min_length=8)

class User(UserBase):
"""Schema for user response."""
id: int
created_at: datetime

class Config:
from_attributes = True

# app/services.py
from typing import List, Optional
from sqlalchemy.orm import Session
from . import models, schemas

def get_user_by_id(db: Session, user_id: int) -> Optional[models.User]:
"""Get user by ID."""
return db.query(models.User).filter(models.User.id == user_id).first()

def get_users(
db: Session,
skip: int = 0,
limit: int = 100
) -> List[models.User]:
"""Get list of users."""
return db.query(models.User).offset(skip).limit(limit).all()

def create_user(db: Session, user: schemas.UserCreate) -> models.User:
"""Create a new user."""
db_user = models.User(
username=user.username,
email=user.email,
full_name=user.full_name
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user

Running Mypy

# Run Mypy on a file
mypy app/main.py

# Run on entire directory
mypy app/

# Check specific module
mypy -m app.main

# Show error codes
mypy app/ --show-error-codes

# Strict mode
mypy app/ --strict

Mypy Configuration (mypy.ini)

[mypy]
# Python version
python_version = 3.11

# Import discovery
files = app/
namespace_packages = True
explicit_package_bases = True

# Strictness
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_generics = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = False
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True

# Error messages
show_error_context = True
show_column_numbers = True
show_error_codes = True
pretty = True

# Ignore missing imports for third-party libraries
[mypy-sqlalchemy.*]
ignore_missing_imports = True

[mypy-passlib.*]
ignore_missing_imports = True

[mypy-jose.*]
ignore_missing_imports = True

Common Mypy Issues in FastAPI

# ❌ Bad: Missing return type annotation
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
return user

# ✅ Good: With return type
from typing import Optional

@app.get("/users/{user_id}")
async def get_user(
user_id: int,
db: Session = Depends(get_db)
) -> Optional[User]:
user = db.query(User).filter(User.id == user_id).first()
return user

# ❌ Bad: Incompatible return type
from typing import List

@app.get("/users/")
async def get_users() -> List[User]:
return {"users": []} # Error: Expected List[User], got dict

# ✅ Good: Correct return type
@app.get("/users/")
async def get_users() -> List[User]:
return []

# ❌ Bad: Missing type annotation in function
def process_data(data):
return data.upper()

# ✅ Good: Complete type annotations
def process_data(data: str) -> str:
return data.upper()

# ❌ Bad: Any type (too permissive)
from typing import Any

def handle_request(data: Any) -> Any:
return data

# ✅ Good: Specific types
from typing import Dict, Union

def handle_request(data: Dict[str, Union[str, int]]) -> Dict[str, str]:
return {k: str(v) for k, v in data.items()}

# ❌ Bad: Untyped async function
async def fetch_data(url):
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()

# ✅ Good: Properly typed async function
from typing import Dict, Any
import httpx

async def fetch_data(url: str) -> Dict[str, Any]:
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()

FastAPI-Specific Type Checking

from fastapi import FastAPI, Depends, HTTPException, status
from typing import Annotated
from sqlalchemy.orm import Session

app = FastAPI()

# Using Annotated for better type hints (Python 3.9+)
def get_db() -> Generator[Session, None, None]:
"""Database dependency."""
db = SessionLocal()
try:
yield db
finally:
db.close()

# Type hint for dependency
DbSession = Annotated[Session, Depends(get_db)]

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: DbSession) -> schemas.User:
"""Get user by ID with proper type hints."""
user = db.query(models.User).filter(models.User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user

# Response model ensures type safety
@app.post("/users/", response_model=schemas.User)
async def create_user(
user: schemas.UserCreate,
db: DbSession
) -> models.User:
"""Create user with type-safe response."""
return create_user_service(db, user)

Mypy Pros and Cons

Pros:

  • ✅ Catches type-related bugs before runtime
  • ✅ Perfect for FastAPI (heavy type hint usage)
  • ✅ Improves code documentation
  • ✅ Great IDE support and autocomplete
  • ✅ Gradual typing (can adopt incrementally)

Cons:

  • ❌ Requires type annotations everywhere
  • ❌ Can be strict in strict mode
  • ❌ Learning curve for advanced types
  • ❌ Some third-party libraries lack type stubs

6. Combining All Three Linters

Pre-commit Configuration

Create .pre-commit-config.yaml:

repos:
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
args: ['--config=.flake8']

- repo: https://github.com/pylint-dev/pylint
rev: v3.0.0
hooks:
- id: pylint
args: ['--rcfile=.pylintrc']

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.0
hooks:
- id: mypy
args: ['--config-file=mypy.ini']
additional_dependencies: [types-all]

Install pre-commit:

pip install pre-commit
pre-commit install

Makefile for Linting

Create a Makefile:

.PHONY: lint lint-pylint lint-flake8 lint-mypy lint-fix

# Run all linters
lint: lint-flake8 lint-pylint lint-mypy
@echo "All linting checks passed!"

# Run Flake8 (fastest, run first)
lint-flake8:
@echo "Running Flake8..."
flake8 app/ tests/

# Run Pylint
lint-pylint:
@echo "Running Pylint..."
pylint app/ tests/

# Run Mypy
lint-mypy:
@echo "Running Mypy..."
mypy app/

# Auto-fix some issues
lint-fix:
@echo "Auto-fixing with autopep8..."
autopep8 --in-place --aggressive --aggressive -r app/
@echo "Auto-fixing with black..."
black app/ tests/

Usage:

# Run all linters
make lint

# Run individual linter
make lint-flake8
make lint-pylint
make lint-mypy

# Auto-fix some issues
make lint-fix

CI/CD Integration (GitHub Actions)

Create .github/workflows/lint.yml:

name: Lint

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Run Flake8
run: flake8 app/ tests/

- name: Run Pylint
run: pylint app/ tests/

- name: Run Mypy
run: mypy app/

7. Complete FastAPI Example with All Linters

Here's a complete FastAPI application that passes all three linters:

# app/main.py
"""
Main FastAPI application entry point.

This module initializes the FastAPI app and includes all routes.
"""
from typing import Dict
from fastapi import FastAPI
from app.routes import users

app = FastAPI(
title="User Management API",
description="A FastAPI application with proper linting",
version="1.0.0"
)

# Include routers
app.include_router(users.router, prefix="/api/v1", tags=["users"])


@app.get("/", response_model=Dict[str, str])
async def root() -> Dict[str, str]:
"""
Root endpoint that returns API information.

Returns:
Dictionary with welcome message
"""
return {
"message": "Welcome to User Management API",
"docs": "/docs",
"version": "1.0.0"
}


@app.get("/health", response_model=Dict[str, str])
async def health_check() -> Dict[str, str]:
"""
Health check endpoint.

Returns:
Dictionary with status
"""
return {"status": "healthy"}
# app/routes/users.py
"""
User routes for the API.

This module contains all endpoints related to user management.
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import schemas, services
from app.database import get_db

router = APIRouter()


@router.get("/users/", response_model=List[schemas.User])
async def get_users(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
) -> List[schemas.User]:
"""
Retrieve a list of users.

Args:
skip: Number of records to skip
limit: Maximum number of records to return
db: Database session

Returns:
List of users
"""
users = services.get_users(db, skip=skip, limit=limit)
return users


@router.post("/users/", response_model=schemas.User, status_code=status.HTTP_201_CREATED)
async def create_user(
user: schemas.UserCreate,
db: Session = Depends(get_db)
) -> schemas.User:
"""
Create a new user.

Args:
user: User creation data
db: Database session

Returns:
Created user

Raises:
HTTPException: If user already exists
"""
db_user = services.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
return services.create_user(db=db, user=user)


@router.get("/users/{user_id}", response_model=schemas.User)
async def get_user(
user_id: int,
db: Session = Depends(get_db)
) -> schemas.User:
"""
Get user by ID.

Args:
user_id: User ID
db: Database session

Returns:
User information

Raises:
HTTPException: If user not found
"""
db_user = services.get_user_by_id(db, user_id=user_id)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return db_user

8. Performance Comparison

Linting Speed Test (on a 10,000 line FastAPI project)

LinterTimeMemoryErrors Found
Flake82-3 seconds~50MB45 issues
Mypy5-8 seconds~150MB23 type issues
Pylint15-20 seconds~200MB78 issues

For optimal developer experience, run linters in this order:

  1. Flake8 (fastest, catches obvious issues)
  2. Mypy (medium speed, type safety)
  3. Pylint (slowest, most comprehensive)

9. Best Practices and Recommendations

When to Use Each Linter

Use Pylint When:

  • ✅ You want comprehensive code quality checks
  • ✅ You're working on a large, complex project
  • ✅ Code maintainability is a priority
  • ✅ You have time for detailed reviews

Use Flake8 When:

  • ✅ You need fast feedback in development
  • ✅ PEP 8 compliance is important
  • ✅ You're setting up CI/CD pipelines
  • ✅ You want low overhead

Use Mypy When:

  • ✅ You're using type hints extensively (like in FastAPI)
  • ✅ You want to catch type-related bugs early
  • ✅ You need better IDE autocomplete
  • ✅ You're building API services

For a production FastAPI project, use all three:

# Development: Quick feedback
make lint-flake8

# Before commit: Type safety
make lint-mypy

# Before push: Full check
make lint

Configuration Tips

  1. Start Lenient: Don't enable all strict checks immediately
  2. Ignore Legacy Code: Use per-file ignores for old code
  3. Team Agreement: Agree on configuration as a team
  4. CI/CD: Run linters automatically on every PR
  5. Pre-commit: Install pre-commit hooks for immediate feedback

10. Conclusion

Summary

AspectPylintFlake8Mypy
Best ForCode qualityStyle complianceType safety
SpeedSlowFastMedium
FastAPI FitGoodGoodExcellent
Learning CurveSteepGentleMedium
Setup TimeLongShortMedium

Final Recommendation

For a production FastAPI project, use all three linters:

  1. Flake8: For quick style checks during development
  2. Mypy: For type safety (essential for FastAPI)
  3. Pylint: For comprehensive code quality reviews

Start with Flake8, add Mypy early, and introduce Pylint gradually as your team gets comfortable with linting.

Quick Start Command

# Install all linters
pip install pylint flake8 mypy

# Create configurations (copy from examples above)
# .pylintrc, .flake8, mypy.ini

# Run all linters
flake8 app/ && mypy app/ && pylint app/

# Or use the Makefile approach
make lint

By combining these three powerful linting tools, you'll maintain high code quality, catch bugs early, and build more reliable FastAPI applications. The initial setup time is well worth the long-term benefits of cleaner, safer, and more maintainable code.