Skip to main content
โšก Calmops

Code Formatting and Linting: Complete Guide to Building Consistent Codebases

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

  1. Install plugins: Black, Ruff, Prettier
  2. Configure Black: Settings โ†’ Tools โ†’ External Tools โ†’ Add Black
  3. Configure Ruff: Settings โ†’ Tools โ†’ External Tools โ†’ Add Ruff
  4. 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:

  1. Use Black/Prettier - Let formatters handle formatting automatically
  2. Use Ruff/ESLint - Catch errors and enforce style programmatically
  3. Configure pre-commit hooks - Catch issues before they reach version control
  4. Enforce in CI/CD - Reject PRs that don’t meet standards
  5. Document exceptions - When you must ignore rules, explain why
  6. 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.

Resources

Comments