1919 words
10 minutes
Building a GitHub Action to Lint Python Code with Flake8 and Black

Building a GitHub Action to Automate Python Linting and Formatting with Flake8 and Black#

Maintaining high code quality and consistency is crucial for collaborative software development projects. As codebases grow and teams expand, enforcing coding standards manually becomes increasingly challenging and time-prone. Automated code quality checks integrated into the development workflow ensure that code adheres to predefined style guides and potential errors are identified early in the development cycle.

Two widely adopted tools in the Python ecosystem for this purpose are Flake8 and Black. Flake8 is a linter, a static analysis tool that checks Python code against the PEP 8 style guide, detects logical errors (like unused imports), and assesses code complexity. Black is an opinionated code formatter that automatically reformats Python code to a consistent style, reducing debates over formatting preferences and ensuring uniformity across a project.

Integrating these tools into a Continuous Integration (CI) pipeline allows for automated checks on every code change. GitHub Actions provides a flexible and powerful platform to build such CI pipelines directly within GitHub repositories. A GitHub Action workflow can be configured to run Flake8 and Black checks automatically whenever code is pushed or a pull request is opened, providing immediate feedback on code quality and style compliance.

Why Automate Linting and Formatting?#

Automating code quality checks offers significant advantages for development teams and projects:

  • Consistency: Ensures all code committed to the repository adheres to the same style standards, improving readability and maintainability. Black is particularly effective at enforcing strict, opinionated formatting.
  • Early Error Detection: Flake8 identifies common programming errors and potential issues before the code is even run or reviewed, reducing the likelihood of bugs reaching production.
  • Reduced Review Overhead: Code reviewers can focus on the logic and functionality of the code rather than spending time pointing out style violations or obvious errors that could be caught automatically. Studies have shown that automated checks can reduce the number of trivial comments in code reviews, allowing reviewers to concentrate on more significant architectural or logical issues.
  • Enforcing Standards: Provides a consistent enforcement mechanism for project coding standards, which is particularly valuable in larger teams or open-source projects with many contributors.
  • Faster Feedback Loop: Developers receive immediate feedback on their code changes directly within the GitHub interface, allowing for quick corrections before the code is merged.

Essential Tools: Flake8 and Black#

A brief overview of the tools used in this automated workflow:

Flake8: The Python Linter#

Flake8 is a command-line tool that combines three core libraries:

  • PyFlakes: Checks for logical errors, such as unused imports, undefined variables, and syntax errors.
  • pep8.py (now pycodestyle): Checks for adherence to the PEP 8 style guide, covering aspects like indentation, line length, and naming conventions.
  • Ned Batchelder’s McCabe script: Checks code complexity by analyzing the number of branches in a function.

Flake8 provides a comprehensive suite of checks to identify potential issues and style violations, resulting in a non-zero exit code if problems are found, which signals a failure in the CI pipeline.

Black: The Opinionated Formatter#

Black is a deterministic formatter. When run on Python code, it reformats the code according to its fixed, opinionated style. Unlike linters that report violations, Black changes the code itself to comply with the standard. Its key characteristics include:

  • Uncompromising: It has very few configuration options, forcing developers to adopt a single style, which eliminates style debates within a team.
  • Deterministic: Running Black multiple times on the same code produces the exact same output, provided no manual changes are made between runs.
  • PEP 8 Compliance: While opinionated, Black generally adheres to PEP 8 conventions, particularly regarding line length (defaulting to 88 characters, with an option for 100 or more).

By using Black in a CI workflow with its --check flag, the action can verify if the committed code already conforms to Black’s style. If it does not, the check fails, indicating that the developer needs to run Black locally on their code.

Introducing GitHub Actions#

GitHub Actions is a CI/CD platform that enables automation of software workflows directly within a GitHub repository. Workflows are defined using YAML files placed in the .github/workflows directory.

A workflow is composed of one or more jobs. Each job runs in a virtual environment (like Ubuntu, Windows, or macOS) and consists of a sequence of steps. Steps can run commands, set up environments, or use pre-built actions from the GitHub Marketplace.

For automating Python linting and formatting, a workflow is configured to trigger on specific events (like push or pull_request), set up a Python environment, install the necessary tools (Flake8 and Black), and then execute the linter and formatter checks using command-line steps.

Building the GitHub Action Workflow#

Creating the GitHub Action involves defining a workflow in a YAML file within the .github/workflows directory of the Python project’s repository.

Workflow File Structure#

A typical workflow file for this purpose might look like this:

.github/workflows/lint_and_format.yml
name: Python Lint and Format Check
on:
push:
branches: [ main, master ] # Trigger on push to main or master
pull_request:
branches: [ main, master ] # Trigger on pull request targeting main or master
jobs:
lint-format-check:
runs-on: ubuntu-latest # Specify the operating system for the job
steps:
- name: Checkout code # Step 1: Get the code from the repository
uses: actions/checkout@v4
- name: Set up Python # Step 2: Configure Python environment
uses: actions/setup-python@v5
with:
python-version: '3.10' # Specify the Python version to use. '3.x' is also an option.
- name: Install dependencies # Step 3: Install Flake8 and Black
run: |
python -m pip install --upgrade pip
pip install flake8 black
- name: Run Flake8 # Step 4: Execute Flake8 checks
run: |
# Stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# Exit-code 0 means no errors, > 0 means problems
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics
- name: Run Black check # Step 5: Execute Black check (does not reformat code)
run: |
# Check if code is formatted according to Black's rules
# The --check flag makes Black exit with a non-zero code if formatting is needed
# The --diff flag shows the proposed changes
black . --check --diff

Breakdown of the Workflow File#

  • name: Python Lint and Format Check: Assigns a human-readable name to the workflow, visible in the GitHub Actions tab.
  • on: ...: Defines the events that trigger this workflow.
    • push: Triggers on code pushes to the specified branches (main, master).
    • pull_request: Triggers when a pull request is opened or updated targeting the specified branches.
  • jobs:: Defines the jobs within the workflow. A workflow can have multiple jobs that run in parallel or sequence.
    • lint-format-check:: The ID of a single job.
    • runs-on: ubuntu-latest: Specifies the virtual environment where the job will execute. ubuntu-latest is a common choice for Python development.
    • steps:: A sequence of tasks to be performed within the job.
      • name: Checkout code: Uses the actions/checkout@v4 action to clone the repository’s code into the runner environment. This is typically the first step in most workflows.
      • name: Set up Python: Uses the actions/setup-python@v5 action to configure a specific Python version. The with: python-version: '3.10' line ensures Python 3.10 is available. This action also adds Python to the system’s PATH.
      • name: Install dependencies: Runs shell commands (run: | ...) to install or upgrade pip and then install flake8 and black using pip. Pinning dependency versions (e.g., flake8==5.0.4 black==22.3.0) is recommended for reproducibility, though pip install flake8 black uses the latest versions by default.
      • name: Run Flake8: Executes the flake8 command.
        • The first flake8 command with --select=E9,F63,F7,F82 checks for common syntax errors, undefined names, etc. An exit code > 0 will fail the step immediately.
        • The second flake8 command runs general checks with --exit-zero (to prevent non-critical violations from failing the step) but still reports --count, --max-complexity, and --max-line-length violations in the logs. Developers can then address these based on the report without necessarily blocking the commit/PR. The specific flags and exit strategy can be adjusted based on project strictness.
      • name: Run Black check: Executes the black command with specific flags.
        • black .: Targets the current directory.
        • --check: This crucial flag tells Black not to change any files, but instead to exit with a non-zero status code if any files would have been reformatted. This makes the step fail if the code is not Black-formatted.
        • --diff: Displays a diff of what changes Black would have made if --check were not used. This is helpful for the developer to see exactly what needs fixing.

Handling Formatting Changes#

The workflow above is designed to verify code formatting. It does not automatically apply Black’s formatting changes to the code and commit them back. This is a standard practice because automatically committing formatting changes from a CI server can create merge conflicts and complicate the commit history.

The intended workflow is:

  1. A developer writes code and makes a commit.
  2. The developer runs black . locally on their machine before committing to format the code.
  3. The developer pushes the formatted code.
  4. The GitHub Action runs black . --check --diff.
  5. If the code was formatted correctly locally (Step 2), the Black check passes.
  6. If the developer forgot to run Black locally, the check fails, and the --diff output shows what needs to be changed. The developer then runs black . locally, commits the formatting changes, and pushes again.

This approach ensures that formatting changes are part of the developer’s commit history and are addressed directly by the developer.

Configuration for Flake8 and Black#

Both Flake8 and Black can be configured to customize their behavior. Common configurations include:

  • Ignoring files or directories: Preventing the tools from checking certain paths (e.g., build artifacts, generated code).
  • Setting line length: Customizing the maximum line length (often done for Black).
  • Ignoring specific error codes: Disabling particular checks in Flake8.

Configuration is typically done using configuration files like pyproject.toml (preferred for modern projects, used by both Black and Flake8), setup.cfg, or .flake8. The tools automatically detect these files in the project root. For example, to set a line length of 100 for both:

pyproject.toml
[tool.black]
line-length = 100
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| build
| dist
)/
'''
[flake8]
max-line-length = 100
exclude = .git,__pycache__,build,dist,venv,.venv,.mypy_cache,.tox
ignore = W503,E501 # Example ignores: line break before binary operator, line too long (if handled by Black)

The GitHub Action workflow automatically respects these configuration files when running flake8 . and black . --check --diff from the project root.

Real-World Application: Pull Request Integration#

The most impactful application of this GitHub Action is its integration with pull requests. When a developer opens or updates a pull request, the workflow automatically runs. The status of the lint-format-check job is displayed prominently on the pull request page.

If the job completes successfully, a green checkmark appears, signaling that the code meets the defined quality and style standards. If the job fails (due to Flake8 errors or Black finding unformatted code), a red cross appears. Developers and reviewers can click on the status to view the detailed output logs from Flake8 and Black, which pinpoint the exact files and lines that need correction.

This immediate, automated feedback loop embedded within the pull request workflow streamlines the review process and ensures that only compliant code is merged into the main branches.

Key Takeaways#

  • Automating Python code linting (Flake8) and formatting (Black) improves code quality, consistency, and maintainability.
  • GitHub Actions provides a platform to integrate these checks into the CI/CD pipeline.
  • A GitHub Action workflow is defined in a YAML file in the .github/workflows directory.
  • The workflow typically checks out the code, sets up a Python environment, installs Flake8 and Black, and then runs the tools.
  • Black should be run with the --check --diff flags in the action to verify formatting without changing files. Failure indicates the developer needs to run Black locally.
  • Configuration for Flake8 and Black can be managed via pyproject.toml or other configuration files.
  • Integrating the action with pull requests provides critical, automated feedback on code changes.
Building a GitHub Action to Lint Python Code with Flake8 and Black
https://dev-resources.site/posts/building-a-github-action-to-lint-python-code-with-flake8-and-black/
Author
Dev-Resources
Published at
2025-06-30
License
CC BY-NC-SA 4.0