Introduction
Consistent code formatting is about more than aesthetics. When every developer follows the same style guidelines, code reviews become more focused on logic and architecture rather than nitpicking formatting choices. Automated linting catches common errors before they reach production, reducing bugs and security vulnerabilities.
This comprehensive guide covers everything you need to establish and maintain code quality standards across your projects. We’ll explore formatting tools for Python and JavaScript/TypeScript, linting configurations, pre-commit hooks for local automation, and CI/CD integration for automated enforcement.
The goal is simple: let machines handle formatting so humans can focus on solving problems. By automating these checks, you reduce cognitive load on developers, speed up code reviews, and maintain consistent quality across your codebase.
Python Code Formatting
Black: The Uncompromising Formatter
Black is the Python community’s go-to formatter. Its “uncompromising” approach means there’s no room for configuration debates - you accept its style or you don’t.
# pyproject.toml
[tool.black]
line-length = 100
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.venv
| \.venv_test
| build
| dist
| __pycache__
| \.mypy_cache
| \.ruff_cache
)/
'''
# Enable for specific files only when needed
[tool.black]
# Format strings that are too long (default: False)
force-line-wrap-prefix-mode = "WrapText"
[tool.black]
# Use the unstable style (unstable style may change between releases)
unstable = false
isort: Import Sorting
Organize imports automatically with isort:
# pyproject.toml
[tool.isort]
profile = "black"
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
skip_gitignore = true
skip = [".venv", "build", "dist"]
# Custom sections for imports
known_first_party = ["mymodule"]
known_third_party = ["requests", "numpy", "pandas"]
# Force specific import order
force_sort_within_sections = true
Ruff: The Fast Python Linter
Ruff is written in Rust for blazing-fast performance:
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"RUF", # Ruff-specific rules
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # do not perform function calls in argument defaults
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*" = ["B011"]
[tool.ruff.lint.isort]
known-first-party = ["mymodule"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
flake8 Configuration
For projects not using Ruff:
# .flake8 or setup.cfg
[flake8]
max-line-length = 100
exclude =
.git,
__pycache__,
.venv,
build,
dist,
*.egg-info
ignore =
E203, # whitespace before ':'
E266, # too many leading '#'
E501, # line too long
W503, # line break before binary operator
per-file-ignores =
__init__.py:F401,F403
max-complexity = 10
mypy: Type Checking
# pyproject.toml
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true
disallow_untyped_calls = false
[[tool.mypy.overrides]]
module = "numpy.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "pandas.*"
ignore_missing_imports = true
JavaScript/TypeScript Formatting
Prettier: Opinionated Code Formatter
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"endOfLine": "lf",
"bracketSpacing": true,
"jsxSingleQuote": false,
"jsxBracketSameLine": false,
"proseWrap": "preserve"
}
ESLint: The JavaScript Linter
// eslint.config.js
import js from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
export default [
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs}'],
rules: {
'no-unused-vars': 'error',
'no-console': 'warn'
}
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
},
plugins: {
'@typescript-eslint': tseslint
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'no-empty': 'warn'
}
}
];
Modern ESLint Flat Config
// eslint.config.mjs
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import react from 'eslint-plugin-react';
import tseslint from 'typescript-eslint';
export default tseslint.config(
// Ignore patterns
{ ignores: ['dist', 'build', 'node_modules', '*.config.js'] },
// Base JavaScript
{
extends: js.configs.recommended,
files: ['**/*.{js,mjs,cjs}'],
languageOptions: {
globals: { ...globals.browser, ...globals.node }
}
},
// TypeScript
...tseslint.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
project: './tsconfig.json'
}
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }]
}
},
// React
{
files: ['**/*.jsx', '**/*.tsx'],
plugins: {
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
'react': react
},
rules: {
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off'
}
}
);
Stylelint: CSS Linting
// .stylelintrc
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order"
],
"rules": {
"color-no-hex": true,
"selector-class-pattern": "^[a-z][a-zA-Z0-9]*(-[a-z][a-zA-Z0-9]*)*$",
"selector-id-pattern": "^[a-z][a-zA-Z0-9]*(-[a-z][a-zA-Z0-9]*)*$",
"property-no-vendor-prefix": true,
"value-keyword-case": "lower"
}
}
Pre-commit Hooks
Pre-commit hooks run checks before code is committed, catching issues early:
# .pre-commit-config.yaml
repos:
# Standard hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: check-toml
# Python formatters
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
language_version: python3.11
# Python import sorting
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: ['--profile', 'black']
# Python linter
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: ['--fix']
- id: ruff-format
# TypeScript/JavaScript
- repo: https://github.com/prettier/prettier
rev: 3.2.4
hooks:
- id: prettier
types_or: [javascript, typescript, jsx, tsx, json, yaml, markdown, html, css, scss]
# Git security
- repo: https://github.com/shunsuke227ono/no-commit-to-branch
rev: v2.0.0
hooks:
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'master', '--branch', 'develop']
# Secret scanning
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.68.0
hooks:
- id: trufflehog
args: ['--no-update']
# Dockerfile linting
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint
# Markdown linting
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.13.1
hooks:
- id: markdownlint
args: ['--fix']
# Enable all hooks in CI
fail_fast: false
CI/CD Integration
GitHub Actions for Python
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install black flake8 isort ruff mypy types-requests
- name: Check formatting (Black)
run: black --check .
- name: Check imports (isort)
run: isort --check-only .
- name: Lint (Ruff)
run: ruff check .
- name: Lint (Flake8)
run: flake8 .
- name: Type check (mypy)
run: mypy .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=. --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
GitHub Actions for Node.js
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v4
GitLab CI for Python
stages:
- lint
- test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
lint:
stage: lint
image: python:3.11-slim
before_script:
- pip install black isort flake8 ruff
script:
- black --check .
- isort --check-only .
- ruff check .
cache:
- key: ${CI_COMMIT_REF_SLUG}
paths:
- .cache/pip
- key: ${CI_COMMIT_REF_SLUG}
paths:
- .venv/
test:
stage: test
image: python:3.11-slim
before_script:
- pip install -e .[dev]
script:
- pytest --cov=. --cov-report=xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
cache:
- key: ${CI_COMMIT_REF_SLUG}
paths:
- .cache/pip
Editor Integration
VS Code Settings
{
// Editor
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"editor.rulers": [100],
"editor.tabSize": 2,
"editor.insertSpaces": true,
// Python
"[python]": {
"editor.defaultFormatter": "ms-python.black",
"editor.formatOnSave": true
},
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.ruffEnabled": true,
"python.formatting.provider": "black",
"python.analysis.typeCheckingMode": "basic",
// TypeScript
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// Prettier
"prettier.requireConfig": true,
"prettier.tabWidth": 2,
"prettier.semi": true,
"prettier.singleQuote": false
}
PyCharm Configuration
- Install plugins: Black, Ruff, Prettier
- Configure Black: Settings โ Tools โ External Tools โ Add Black
- Configure Ruff: Settings โ Tools โ External Tools โ Add Ruff
- Enable format on save: Settings โ Tools โ Actions on Save
Best Practices
| Practice | Implementation |
|---|---|
| Automate everything | Run linters in CI/CD pipeline |
| Be consistent | Use project-wide config files |
| Accept automated fixes | Don’t fight the formatter |
| Configure CI to fail | Reject PRs with violations |
| Use pre-commit hooks | Catch issues before commit |
| Version your config | Keep config in version control |
| Document exceptions | Explain when rules are ignored |
| Keep tools updated | Use latest stable versions |
Code Quality Metrics
Track these metrics to monitor code quality over time:
- Cyclomatic complexity: Measure code complexity
- Lines of code per function: Identify oversized functions
- Code duplication: Find repeated code
- Comment ratio: Balance documentation
- Test coverage: Ensure adequate testing
# Example: Track complexity with pylint
[pylint]
max-complexity = 10
min-public-methods = 0
# Example: Track with SonarQube
# sonar-project.properties
sonar.projectKey=my-project
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
Conclusion
Establishing consistent code formatting and linting practices is one of the highest-impact improvements you can make to your development workflow. The initial setup investment pays dividends in reduced code review cycles, fewer bugs, and more maintainable codebases.
Key takeaways:
- Use Black/Prettier - Let formatters handle formatting automatically
- Use Ruff/ESLint - Catch errors and enforce style programmatically
- Configure pre-commit hooks - Catch issues before they reach version control
- Enforce in CI/CD - Reject PRs that don’t meet standards
- Document exceptions - When you must ignore rules, explain why
- Keep tools updated - New versions often catch more issues
By implementing these practices, you’ll create a development environment where code quality is maintained automatically, allowing developers to focus on solving problems rather than arguing about formatting.
Comments