Skip to main content

Linting & Code Quality in GitHub Actions Workflows

Code linting and formatting checks catch style violations, unused imports, and potential bugs before they enter your codebase. When linting runs automatically in GitHub Actions on every pull request, teams stay aligned on code standards without manual code review overhead.

This guide covers three complementary tools: flake8 for style and error detection, Black for automatic code formatting, and isort for import organization. You'll configure them in your workflow to fail builds on violations, format code automatically, and integrate results into pull request checks.

Understanding Linting vs. Formatting

Linting detects violations of style rules and potential errors without modifying code. Tools like flake8 report issues but don't fix them. Formatting tools like Black automatically reformat code to conform to a standard style. isort organizes imports alphabetically and in groups. Running all three ensures consistent, bug-free code.

flake8 checks against PEP 8, the Python style guide. Black enforces a stricter, more opinionated style (line length 88 characters) designed to eliminate formatting debates. isort follows a standard import order (standard library, third-party, local).

Installing Linting Tools

Add linting dependencies to your requirements-dev.txt:

flake8>=6.0
black>=23.0
isort>=5.0

Then install them in your workflow:

name: Lint and Format

on: [push, pull_request]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install flake8 black isort
- name: Check code style with flake8
run: flake8 src/ tests/ --count --show-source --statistics
- name: Check import order with isort
run: isort --check-only src/ tests/
- name: Check formatting with Black
run: black --check src/ tests/

The --check-only flag tells isort and Black to check without modifying files. flake8 always reports without modifying.

Configuring flake8

Create a .flake8 file in your repository root to customize flake8 behavior:

[flake8]
max-line-length = 100
exclude = .git,__pycache__,build,dist,.venv
ignore = E203,W503
per-file-ignores =
__init__.py:F401
tests/*:F401,E501

This configuration sets a maximum line length of 100 characters, excludes build directories and virtual environments, and ignores specific error codes. F401 is unused imports (ignored in __init__.py and test files) and E501 is line too long (ignored in tests where readability is less critical).

Configuring Black

Create a pyproject.toml file to configure Black:

[tool.black]
line-length = 88
target-version = ['py311']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| build
| dist
)/
'''

Black's default line length is 88, which you can override to match flake8's max-line-length. Keep them in sync to avoid conflicts.

Configuring isort

Add isort configuration to pyproject.toml:

[tool.isort]
profile = "black"
line_length = 88
multi_line_mode = 3
include_trailing_comma = true

The profile = "black" setting makes isort compatible with Black's formatting. multi_line_mode = 3 uses a trailing comma style that Black understands.

Auto-Formatting Code in Workflows

While linting only checks, you can configure workflows to automatically format code and commit the changes back to the pull request. This reduces friction:

- name: Format code with Black and isort
run: |
black src/ tests/
isort src/ tests/
- name: Commit formatting changes
if: failure()
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: auto-format code with Black and isort"
commit_options: "--no-verify"
file_pattern: "src/ tests/"

The git-auto-commit-action commits formatted changes automatically. Use if: failure() to run this only if the format check failed; otherwise you'll create empty commits.

Integrating with Pull Request Comments

Combine flake8 output with a community action to post comments on pull requests:

- name: Run flake8 and save output
id: flake8
run: |
flake8 src/ tests/ --output-file=flake8-report.txt || true
- name: Post flake8 report as comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('flake8-report.txt', 'utf8');
if (output.trim()) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '```\n' + output + '\n```'
});
}

This posts flake8 violations as a comment on the pull request, giving developers immediate feedback without leaving GitHub.

Key Takeaways

  • flake8 detects style violations and errors; Black auto-formats code; isort organizes imports.
  • Configure all three tools in the workflow to fail on violations, catching issues before merge.
  • Use .flake8, pyproject.toml, and pyproject.toml respectively to customize each tool's behavior.
  • Keep flake8's max-line-length and Black's line-length in sync to avoid conflicts.
  • Use git-auto-commit-action to automatically format code and commit changes back to pull requests.
  • Post linting violations as pull request comments for immediate developer feedback.

Frequently Asked Questions

Can I make Black and flake8 work together without conflicts?

Yes, but only if they use the same line-length setting. Set both to 88 (Black's default) or 100 (common for flake8). Black ignores certain flake8 rules by design (like E203), so add those to flake8's ignore list.

What if my codebase has existing style violations?

Run Black and isort once to format the entire codebase, then commit. From then on, the workflow enforces the standard. For large codebases, this one-time reformatting often changes hundreds of files, so do it in a dedicated PR.

Should I auto-commit formatting or fail the build?

For new projects or small teams, auto-commit is convenient. For large teams or projects with strict code review processes, fail the build and require developers to run formatters locally before pushing. This keeps them engaged with code quality.

How do I ignore specific files or patterns?

Add them to the exclude list in .flake8 or pyproject.toml. For example, exclude migrations and generated code: exclude = migrations,schema_pb2.py.

What error codes should I ignore?

Common ignores: E203 (whitespace before colon, Black uses this), W503 (line break before binary operator, Black prefers), F401 (unused imports in __init__.py). Never blindly ignore errors; understand each before adding to ignore lists.

Further Reading