Creating software is an achievement worth celebrating. But software is never static. Bugs need to be fixed, features need to be added, and security demands regular updates. In today’s landscape, with agile methodologies dominating, robust DevOps systems are crucial for managing an evolving codebase. That’s where GitHub Actions shine, empowering Python developers to automate workflows and ensure their projects adapt seamlessly to change.
GitHub Actions for Python empowers developers to automate workflows efficiently. This enables teams to maintain software quality while adapting to constant change.
Continuous Integration and Continuous Deployment (CI/CD) systems help produce well-tested, high-quality software and streamline deployment. GitHub Actions makes CI/CD accessible to all, allowing automation and customization of workflows directly in your repository. This free service enables developers to execute their software development processes efficiently, improving productivity and code reliability.
In this tutorial, you’ll learn how to:
- Use GitHub Actions and workflows
- Automate linting, testing, and deployment of a Python project
- Secure credentials used for automation
- Automate security and dependency updates
This tutorial will use an existing codebase, Real Python Reader, as a starting point for which you’ll create a CI/CD pipeline. You can fork the Real Python Reader code on GitHub to follow along. Be sure to deselect the Copy the master branch only option when forking. Alternatively, if you prefer, you can build your own Real Python Reader using a previous tutorial.
In order to get the most out of this tutorial, you should be comfortable with pip, building Python packages, Git, and have some familiarity with YAML syntax.
Before you dig into GitHub Actions, it may be helpful to take a step back and learn about the benefits of CI/CD. This will help you understand the kinds of problems that GitHub Actions can solve.
Get Your Code: Click here to download the free sample code you’ll use to learn about CI/CD for Python With GitHub Actions.
Take the Quiz: Test your knowledge with our interactive “GitHub Actions for Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
GitHub Actions for PythonIn this quiz, you'll test your understanding of GitHub Actions for Python. By working through this quiz, you'll revisit how to use GitHub Actions and workflows to automate linting, testing, and deployment of a Python project.
Unlocking the Benefits of CI/CD
Continuous Integration (CI) and Continuous Deployment (CD), commonly known as CI/CD, are essential practices in modern software development. These practices automate the integration of code changes, the execution of tests, and the deployment of applications. This helps teams and open-source contributors to deliver code changes more frequently in a reliable and structured way.
Moreover, when publishing open-source Python packages, CI/CD will ensure that all pull requests (PRs) and contributions to your package will meet the needs of the project while standardizing the code quality.
Note: To learn more about what a pull request is and how to create one, you can read GitHub’s official documentation.
More frequent deployments with smaller code changes reduce the risk of unintended breaking changes that can occur with larger, more complex releases. For example, even though developers can format all code using the same linting tools and rules, policy can automatically block PRs from being merged if the code’s tests don’t pass.
In the next section, you’ll learn how GitHub Workflows can help you implement CI/CD on a repository hosted on GitHub.
Exploring GitHub Workflows
GitHub Workflows are a powerful feature of GitHub Actions. They allow you to define custom automation workflows for your repositories. Whether you want to build, test, or deploy your code, GitHub Workflows provide a flexible and customizable solution that any project on GitHub can use for free, whether the repository is public or private.
Even though there are many CI/CD providers, GitHub Actions has become the default among open-source projects on GitHub because of its expansive ecosystem, flexibility, and low or no cost.
Anatomy of a Workflow File
Workflow files are declaratively written YAML files with a predefined structure that must be adhered to for a workflow to run successfully. Your YAML workflow files are stored and defined in a .github/workflows/ folder in your project’s root directory.
Your workflow folder can have multiple workflow files, each of which will perform a certain task. You can name these workflow files anything you’d like. However, for the sake of simplicity and readability, it’s common practice to name them after the tasks they achieve, such as test.yml.
Each file has a few elements that are required, but many, many more that are optional. The GitHub Actions documentation is thorough and well-written, so be sure to check it out after you’ve finished reading this tutorial.
There are three main parts that make up the bulk of a workflow file: triggers, jobs, and steps. You’ll cover these in the next sections.
Workflow Triggers
A trigger is an event that causes a workflow to run. There are many kinds of triggers. The most common ones are those that occur on a:
- Pull request
- Pushed commit to the default branch
- Tagged commit
- Manual trigger
- Request by another workflow
- New issue being opened
You might also want to restrict triggers further by limiting it to a specific branch or set of files. Here’s a simple example of a trigger that runs a workflow on any push to the main branch:
.github/workflows/example.yml
on:
push:
branches:
- main
For detailed information about the triggers not covered in this tutorial, you can check out the official documentation.
Now that you know how events trigger workflows, it’s time to explore the next component of a workflow file: jobs.
Workflow Jobs
Each workflow has a single jobs section, which is the container for the meat and potatoes of the workflow. A workflow can include one or more jobs that it will run, and each job can contain one or more steps.
Here’s an example of what this section would look like without any steps:
.github/workflows/example.yml
# ...
jobs:
my_first_job:
name: My first job
my_second_job:
name: My second job
When you’re creating a job, the first thing to do is define the runner you want to use to run your job. A runner is a GitHub-hosted virtual machine (VM) that executes your jobs for you. GitHub will provision and de-provision the VM so you don’t have to worry about maintaining any infrastructure for your CI/CD.
There are multiple supported operating systems available. You can find the full list of GitHub-hosted runners in the documentation.
Note: Self-hosted runners are also an option if the free and unlimited versions don’t meet your needs. This tutorial doesn’t cover self-hosted runners, but you can find detailed information about using self-hosted runners in the official documentation.
Defining a runner takes as little as a single line of YAML:
.github/workflows/example.yml
# ...
jobs:
my_first_job:
name: My first job
runs-on: ubuntu-latest
# ...
my_second_job:
name: My second job
runs-on: windows-latest
# ...
In the above example, my_first_job will run inside an Ubuntu VM, and my_second_job will run inside a Windows VM. Both use the -latest suffix in this case, but you could also specify the exact version of the operating system—for example, ubuntu-20.24, as long as it’s a supported version.
Workflow Steps
Steps are the main part of a job. As you’ve probably guessed, the steps declare the actions that need to be performed when executing the workflow. This can include tasks such as installing Python, running tests, linting your code, or using another GitHub action.
Just like your Python code, common and repeatable tasks can be abstracted away into separate workflows and reused. This means you can and should use other people’s GitHub Actions in your own workflows, similar to how you would when importing a Python library, to save you time reimplementing that functionality.
In the next section, you’ll see how you can use other GitHub Actions and how to find them.
Using GitHub Actions for Python
Even though workflows are a part of GitHub Actions, workflows can also contain GitHub Actions. In other words, you can use other people’s or organization’s actions in your workflow. In fact, it’s common practice and highly encouraged to use existing GitHub Actions in your workflow files. This practice saves you time and effort by leveraging pre-built functionalities.
If you have a specific task to accomplish, there’s likely a GitHub Action available to do it. You can find relevant GitHub Actions in the GitHub Marketplace, which you’ll dive into next.
Exploring the GitHub Marketplace
The GitHub Marketplace is an online repository of all the actions people can use in their own workflows. GitHub, third-party vendors, and individuals build and maintain these GitHub Actions. Anyone can use the GitHub Action template to create their own action and host it in the marketplace.
This has led to a vast array of GitHub Actions available for nearly every type of task automation imaginable. All actions in the GitHub Marketplace are open source and free to use.
In the next section, you’ll look at two GitHub Actions that you’ll use for every Python project.
Including Actions in Workflows
Every Python-based workflow you create needs to not only check out your current repository into the workflow environment but also install and set up Python. Fortunately, GitHub has official GitHub Actions to help with both tasks:
.github/workflows/example.yml
# ...
jobs:
my_first_job:
name: My first job
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: python -m pip install -r requirements.txt
In the example above, you can see that the first step in steps is to use the official checkout action. This action checks out the code from your repository into the current GitHub workspace, allowing your workflow to access it. The @4 following checkout is a version specifier, indicating which version of the action to use. As of now, the latest version is v4.2.2, so you can refer to it using this syntax to specify the latest major version.
The second step of this example sets up Python in the environment. Again, this example uses the official GitHub Action to do this because of its ongoing support and development. Most actions, if not all, have extra configurations you can add to the step.
The Setup Python action documentation contains the complete list of configurations. For now, the minimum you need to install Python into your workflow environment is to declare which version of Python you wish to install.
In the final step of the example, you use the run command. This command allows you to execute any bash or powershell command, depending on which runner you’re using for the step. In this case, you’re installing the project’s dependencies from the requirements file.
Hopefully, you can see how powerful GitHub Actions can be. With very little code and effort, you have a reproducible way to set up an environment that’s ready for building, testing, and deploying your Python project.
You now have a basic understanding of the structure of a workflow file and how you can create your first workflow for a project. In the next section, you’ll do just that with a real-world example.
Creating Your First Workflow
It’s time to walk through the steps of adding CI/CD to an existing real-world project, the Real Python Reader. Before you add workflows for testing and deploying this package, you should first start with linting.
A linter is a tool that analyzes your code and looks for errors, stylistic issues, and suspicious constructs. Linting allows you to address issues and improve your code quality before you share it with others. By starting your CI/CD with linting, you’ll ensure that your code is clean and readable before deploying the package to PyPI.
Note: If linting is a new concept for you, then you can learn more about it by reading about Ruff, a modern Python linter.
For this workflow, you’ll use Ruff to lint the Python code. But if you haven’t already, first fork the repository, including all branches, and then clone it. Be sure to replace your-username with your GitHub username:
$ git clone git@github.com:your-username/reader.git
$ cd reader/
$ git checkout github-actions-tutorial
$ mkdir -p .github/workflows/
After you clone your forked repository and change your current working directory, you’ll need to switch to the pre-existing branch named github-actions-tutorial. If such a branch is unavailable, then you most likely forgot to uncheck the Copy the master branch only option when forking. In such a case, you should delete your fork, go back to the original repository, fork it again, and ensure that you include all branches this time.
Once you’ve successfully switched to the correct branch, create a folder to store your workflows. This folder should be called workflows/ and be a subdirectory of the .github/ folder.
Note: When you fork a repository that has existing GitHub Actions, you might see a prompt asking you to enable them after you click on the Actions tab of your forked repository. This is a safety feature. By confirming that you want to enable the actions, you won’t have any issues following the rest of this tutorial.
Now, you’re ready to create your first workflow where you’ll define your triggers, set up the environment, and install Ruff. To start, you can define your triggers in the lint.yml file:
.github/workflows/lint.yml
1name: Lint Python Code
2
3on:
4 pull_request:
5 branches:
6 - master
7 push:
8 branches:
9 - master
10 workflow_dispatch:
Even though it’s not required, it’s considered best practice to give each of your workflows a clear, human-readable name. This name will appear in the left column of the Actions tab on your GitHub repository. It helps you identify the available workflows and filter through your previous workflow runs:

After defining the name, you can shift your focus to the triggers for this workflow. In the code above, there are three different triggers defined that can initiate the workflow:
- Opening a pull request
- Pushing local commits
- Dispatching the workflow manually
The first two will trigger the workflow on any push or pull request event on the master branch. This means that any change to the code will trigger this workflow to run, whether you push straight to master, or use a pull request to merge code into the master branch on your repository.
Note: This workflow gets triggered by events on the master branch while you’re working on another branch. If you’d like to see the action take effect immediately after pushing your commits to GitHub, then consider adding github-actions-tutorial to the list of branches monitored by the workflow.
It’s not obvious what the final trigger does. According to the documentation, it’s commonly used to rerun a workflow that failed for reasons unrelated to code changes, such as an expired API key. However, the workflow_dispatch trigger only works when the workflow file is on the default branch.
With the triggers defined, it’s time to proceed to the next step in creating the workflow file, which is to define the jobs and configure the environment:
.github/workflows/lint.yml
1name: Lint Python Code
2
3on:
4 pull_request:
5 branches:
6 - master