Hello! I created this repo to track a blog post series about Flask, but I never finished it and never will. I actually stopped using Flask in favor of FastAPI and recommend you do the same for a truly wonderful experience in creating Python REST APIs.
How this Post Works
- This post is part of a series which dissects my method for creating a REST API using Python and Flask.
- Each post is written under the assumption that the reader has read and understood all previous posts in the series.
- I will attempt to explain every choice made along the way. If you feel I've left something out please mention it in a comment below.
- Each section explains why and how to use a specific technique or technology. Feel free to skip sections you're already comfortable with.
- The source code generated for this series can be found on GitHub.
In This Post
This post walks through creating a very simple Flask app using the factory pattern. It then creates a simple health check controller in a Blueprint by using Test Driven Development with pytest. At the end of this post, you have an extremely simple webserver with complete test coverage!
The Topics:
- Flask Factory Pattern
- Docstrings
- Type Annotations
- Test Driven Development
- pytest
- Flask Blueprints
Flask Factory Pattern
The entirety of Flask is centered around the Flask
app object, the first thing you have to do to start using Flask is to create one of these objects somewhere in your project. There are two common methods for doing this that you'll see in tutorials:
Create the object in a file (usually called "app.py") which will later be imported by whatever server is serving it. The primary benefit to this method is that there is less boilerplate and you can get up and running quickly, thus why it's a popular choice for tutorials.
-
Declare a "factory method" which returns an instance of an app. This method requires a bit more typing, but has a lot of benefits:
- Delay imports by putting them in the factory method to avoid circular dependencies (very important when you start using extensions)
- Delay initialization of extensions (makes patching easier for unit tests)
- Allows you to dynamically configure the app for different use-cases (e.g. testing vs development vs production)
- Many more, check out the docs here
I always recommend using the factory method, it's pretty easy to get started. First, create the method itself in the base __init__.py
for your module.
# python_rest/__init__.py
from flask import Flask
def create_app():
return Flask(__name__)
That's all the code there is for declaring the app! Now we just need to run it with the Flask development server.
Running on the command line
We can easily run the app with Poetry on the command line, but first we have to tell Flask what to run. Luckily, it comes with built in support for environment files which let us declare all sorts of stuff. First, add python-dotenv
as a development dependency.
$ poetry add -D python-dotenv
Now create a file in the root of the project called ".flaskenv" which sets the environment variable "FLASK_APP" to the name of your module.
Note: The Flask recommendation is to check the ".flaskenv" file into the project with any non-sensitive, default values. You can then override them with the more standard ".env" file which should never be checked into source control.
# .flaskenv
FLASK_APP=python_rest
Now you're ready to run the development server. Make sure you run it under Poetry!
$ poetry run flask run
Running in PyCharm
- At the top of the screen, click "Add Configuration"
- Click the "+"
- Choose "Flask server"
- Set "Target type" to "Module name"
- Set "Target" to your module name (e.g. "python_rest")
- Click "OK"
- With your new configuration selected, click the green arrow to start running
🎉 Hooray! You now have a webserver running on port 5000 (by default) of your local machine! It doesn't do anything yet, but I promise it's running.
Docstrings
Now that you've written a Python function, you should definitely write a docstring for that function. The reasons to document your code are endless, I find it best practice to give every function a docstring with at very least a couple word summary. Docstrings will be important later on for creating good documentation for your API, so it's best to get in the habit right off.
There are a bunch of formats out there for docstrings, most are supported by and can be rendered in PyCharm. This page gives a nice overview of the different formats. I use ReStructuredText strings just because they're natively supported by Sphinx which I use in other types of projects. They look something like this:
def create_app():
"""
Creates an application instance to run
:return: A Flask object
"""
return Flask(__name__)
Type Annotations
I cannot stress how important I think type annotations are. Python's dynamic typing system is awesome and makes writing code very fast. However, it can easily lead you to making simple mistakes as your project becomes more complex. I type hint all the things so that PyCharm can tell me when I'm doing something dumb (like forgetting a return somewhere) and so a static analysis tool (like mypy) can catch some common bugs. The type annotation for our create_app
function looks like this:
def create_app() -> Flask:
Test Driven Development
Test Driven Development, or TDD, is very simply the act of writing tests for your code before you write the code itself. There are entire books written on why this is a good idea, but very simply, this is why I do it:
- You always know when you're done
- You can refactor with more confidence
- Helps you to think through edge cases and unhappy paths before you start
- Encourages modular and decoupled designs
Pytest
In order to write tests, you need to use some sort of framework. There is one included in the Python standard library called "Unittest" but I much prefer pytest. There are a bunch of reasons to use it but the basics are:
- Very little boilerplate
- Loads of community plugins
One of the aforementioned plugins that I always use while testing Flask apps is pytest-flask which provides a bunch of useful fixtures. To get started with testing, install both pytest and pytest-flask as development dependencies.
$ poetry add -D pytest pytest-flask
Now, if you're using PyCharm, you'll want to configure it to use pytest
- Open Preferences
- Click Tools
- Go to Python Integrated Tools
- Change "Default test runner" to "pytest"
Note: you can change Docstring format here too
Next, create the configuration file for pytest (called conftest.py) in the tests dir. This file is always imported before tests are run and it's where you'll want to define any fixtures you intend on using and perform any other pre-test setup. To use pytest-flask, we have to define an "app" fixture which returns an instance of our Flask
object.
# tests/conftest.py
import pytest
from flask import Flask
@pytest.fixture
def app() -> Flask:
""" Provides an instance of our Flask app """
from python_rest import create_app
return create_app()
Blueprints
Now it's time to create an endpoint that we can hit. A very common practice is to have some sort of health check endpoint to verify that your application is running. In order to create this endpoint, we'll need to create a controller (the function which handles the request) which will be registered to a blueprint (a collection of controllers under a path) which will in turn be registered to our app object.
First, let's figure out where to put this new code. I like to keep any blueprints I create in a module called "blueprints" under the main module. This blueprint will be for managing the root path ("/") so I'll put it in a file called "root.py" under the "blueprints" module.
Next, we should mirror this directory structure in the tests directory by creating "tests/test_blueprints/test_root.py". Along with the appropriate __init__.py
files, the whole project structure should now look like this:
python-rest
|-- README.md
|-- poetry.lock
|-- pyproject.toml
|-- python_rest
| |-- __init__.py
| {% raw %}`-- blueprints
| |-- __init__.py
| `-- root.py
`-- tests
|-- conftest.py
`-- test_blueprints
|-- __init__.py
`-- test_root.py
```
Let's write the test for the endpoint we're about to create. We can use the client fixture provided by pytest-flask to execute the request appropriately. We'll want the health check to return a status of 200 (OK) with some JSON body. Here's the test:
```python
# tests/test_blueprints/test_root.py
from http import HTTPStatus
def test_health(client):
response = client.get('/health/')
assert response.status_code == HTTPStatus.OK, 'Health check failed'
assert response.json == {'message': 'Healthy'}, 'Improper response'
```
Note that those assert statements are the things that will cause the test to pass or fail when run. The string after the comma after the assert is the message that will be displayed if the check fails. Run the test either by clicking the little green arrow next to the function definition in PyCharm, or by running `poetry run pytest` on the command line. Either way, you should see it fail.
Now let's create the root blueprint and the actual endpoint.
```python
# python_rest/blueprints/root.py
from flask import Blueprint
# Declare the blueprint with whatever name you want to give it
root_blueprint = Blueprint('root', __name__)
# This is how you register a controller, it accepts OPTIONS and GET methods by default
@root_blueprint.route('/health/')
def health():
return {'message': 'Healthy'} # This will return as JSON by default with a 200 status code
```
Now we need a way to register this blueprint to the app so we can actually reach the controller. They way to do this is with a function called at app creation which registers the blueprint to the app. The convention is to name this function "init_app", and I like to have one "init_app" function which registers all blueprints at once. The logical thing is to put this function in the `__init__.py` of the blueprints module.
```python
# python_rest/blueprints/__init__.py
from flask import Flask
def init_app(app: Flask):
from .root import root_blueprint
app.register_blueprint(root_blueprint)
```
Now you just need to call this function from your create_app factory function.
```python
# python_rest/__init__.py
from flask import Flask
def create_app() -> Flask:
"""
Creates an application instance to run
:return: A Flask object
"""
app = Flask(__name__)
from . import blueprints
blueprints.init_app(app)
return app
```
That's it! If you run the test again it should now pass! You can also run the flask app, then navigate to `localhost:5000/health/` in your browser to see your JSON response.