Refactor Your Flask App to Scale in 12 Steps

March 16, 2021

So you just created your first Flask application, what’s next? If you’re like me, your app works, but it’s like a baby deer that’s lost its mother—its chances of ever growing old and maturing are slim to none. Let’s fix that. In my opinion, it’s much easier to write something well from the start than it is to go back and fix something crappy; however, the best way to learn is by fixing your mistakes. Therefore, this post will not explain how to write a perfect Flask app from scratch; instead, it will highlight 12 mistakes I made while creating my first major Flask app, explain why these mistakes were prohibitive to growth, and demonstrate how I fixed them.

There are two ways to think about “scalability” in the context of web applications:

  1. If you want to expand the app’s capabilities, can you easily add a new feature or will the code become a mess? Is the code easily revised or revisited?
  2. If your app becomes popular and the number of requests skyrockets can the application still handle all the requests?

Today we’ll be covering the first point, but I’ll tackle the second in part 2 of this post series (stay tuned!). I hope my experiences will serve as a guide for newer programmers who also want to take their Flask code to the next level.

My GitHub repository before refactoring.

My GitHub repository after refactoring.

12 Steps to Refactor Your Flask App to Scale:

The Lifesaver:

  1. Write Tests Early and Often

Configuration:

  1. Sensitive Info Should Not be Included in the Code Base
  2. Keep Your Configuration in One Place
  3. Use an App Factory

Organization:

  1. Separate Unrelated Code into New Modules
  2. Store Your App in a Package
  3. Stay Organized with Blueprints

Good Ideas:

  1. Avoid Unnecessary Plugins
  2. Do Not Copy/Paste Random Code that You Don’t Understand
  3. Keep Your CSS Consistent and Dynamic
  4. Make Sure the Back End and Front End are in Sync
  5. Make the App Easy to Use

Note: At the moment, I have fixed everything except 12.

The Lifesaver

1. Write Tests Early and Often

As the Flask documentation says, “Something that is untested is broken.” Write tests for your current self, but even more so for your future self. Let’s say you’re adding a new feature and something in your code breaks, wouldn’t it be nice to have a solid test suite that can identify exactly what’s going wrong so you don’t have to spend hours debugging? ‘Exactly’ might be a bit of an exaggeration, but tests have saved me lots of time in other projects.

Writing tests can be a large upfront time investment, but it will pay dividends each time you add a new feature or notice a bug.

Here’s an example of a simple test I wrote to make sure that the “Login” page is loaded properly when requested:

Even seemingly simple tests like this are useful. I forgot to update the template path for the login page when I added blueprints and thanks to this test failing I was able to quickly realize my mistake. Learn more about how to write tests for your Flask app.

Configuration

2. Sensitive Info Should Not be Included in the Code Base

Sensitive info such as passwords, secrets, tokens, etc. should never be included in your codebase. Oopsies. The solution is environment variables. For your development environment, there’s a convenient package called Python-dotenv that allows you to store your secrets in a .env file and then access them using ``os.getenv(<var-name>)``. For production, Heroku lets you set environment variables via the CLI or dashboard (they call them “Config Variables”). Make sure you name the variables the same as in the .env file.

Soon I’ll publish a blog post showing how ‘deep clean’ your codebase and remove previously committed sensitive info.

3. Keep Your Configuration in One Place

I had various configuration items for my app hard-coded and scattered throughout the app.py file. This made it hard to adjust and manage. For example, I had a hard-coded variable called “ENV” which I manually changed depending if I wanted to run the app with a production or a development configuration.

Terrible idea. Do you know how many times I had to push a second commit to heroku just because I forgot to change that variable to “PROD”? Many times:

This is another reason why you should look into setting up environment variables. As my Recurse Center colleague Ben Morsillo says, “it's all about keeping your levers and buttons in one place, so you know where to look for them.”

4. Use an App Factory

This is one of the most important points on this list, yet many people don’t understand what an app factory is or why it’s so important. An app factory is basically a function that creates an instance of your Flask app. Most introductory Flask tutorials have you create your app at the top of your app.py file with ``app = Flask(__name__)``, but this doesn’t give you much wiggle room for creating different variations of your app. The better way to do it is to wrap this code in a function called ``create_app()`` that accepts different configurations as arguments. That function, ``create_app()``, is your “App Factory.” With an app factory you can pass in a config file to easily set up different variations of the app instance—such as a production version, a development version and a testing version.

The Flask documentation claims there will be ‘tricky issues’ if you try to scale without an app factory, but it doesn’t go into much detail about what these issues are so let me explain...

Three reasons you should use an app factory:

  1. The app factory setup helps you avoid circular import errors.
  2. It simplifies testing. With an app factory you can pass in a testing configuration that spins up an app instance prepared for testing, i.e. one that connects to a testing database instead of your real one, among other things.
  3. Not using an app factory will likely be a problem if/when you want to publish using most server architecture because most server frameworks, for example Waitress, operate by importing the app factory.

If you were using ‘python app.py’ or similar to run your app, you’ll have to add the following lines of code to the bottom of your app.py file (notice line 2):

Learn more about using an app factory.

Organization

Your tests will come in handy during this section - if you don’t have them set up yet, I highly recommend doing so. Here’s a before/after of what the project folder will look like before and after reorganizing it:

5. Separate Unrelated Code into New Modules

The Flask tutorial I found on Youtube got me off the ground and running, but it taught me nothing about how to organize my codebase. Hence, everything went in the app.py file. I once tried to separate out the models and routes, thinking it’d be helpful for organization, but I got circular import errors and quickly gave up. I actually got a pesky circular import error this time around as well, but fortunately I was able hunt it down and fix it (more on that in another post).

6. Store Your App in a Package

For a larger application, it’s a good idea to store all of the actual application code inside of a folder (I call this folder ‘application’). This not only helps you stay organized, but it also:

  1. Is more conducive to using an app factory and blueprints.
  2. Makes it easier for newcomers to understand and find things in your code base.

At this point if you are still running ‘python app.py’ or similar to create an app instance, it’s time to switch to a more sophisticated method - the ‘flask run’ method (if you were already doing so, just refactor your setup a bit to account for the new organization - you can check out my examples).

To run your app locally:

For deployment on Heroku:

7. Stay Organized with Blueprints

I essentially have two different apps within one app:

  1. The Blog App (read posts)
  2. The Blog Administration App (authentication, post creation, email)

Working with the app would be so much more convenient if the routes and templates for these two ‘mini-apps’ were all separated instead of thrown in the same folder/module.

Learn more about blueprints.

Good Ideas

8. Avoid Unnecessary Plugins.

Maybe this is in error, but my thinking goes like ‘if you don’t have to use a plugin, don’t use a plugin.’ A plugin is a massive black box that you haven’t peeked inside and that could lead to issues down the road. Wouldn’t it be easier to debug code that you wrote specifically for your project as opposed to code someone else wrote for all kinds of projects? I’m not saying avoid plugins at all costs—if you’re not sure how to add a feature to your app and don’t have the time/desire to learn, a plugin can be wonderful.

I used Flask-Login because I didn’t understand how to implement authentication on my own and didn’t want to focus on it at the time; however it was easy to replace. Soon I’ll also remove Flask-Mail.

Note: For now, I plan to keep Flask-SQLAlchemy and Flask-Migrate because I find them convenient, well-maintained and well-documented; however, if you’d like to learn more about replacing Flask-SQLAlchemy check out my article “Under the Hood of Flask-SQLALchemy”.

9. Do Not Copy/Paste Random Code that You Don’t Understand

We’ve all done it. We find a bug and instead of digging deep into what the issue is, we head to StackOverflow and copy paste all the potential solutions we find. Make sure you understand why a solution works before you decide to keep it in your code - if i had done that then I would have realized that some of the code I pasted was never even working in the first place. Do yourself a favor and go back into your code and clean up anything you don’t understand.

In this project I had an issue with the CSS not refreshing when I reloaded the site. Upon further examination, neither of the two code snippets I had copy pasted worked so I removed them (two months later).

10. Keep Your Styling Consistent and Dynamic

My CSS was unnecessarily complicated for a site that’s black and white with a few artfully-placed black lines. Most sites consist of a few core elements that are repeated on various pages (buttons, forms, headers, banners, etc). Identify what these elements are and rework your CSS to make the styling for these consistent with reusable classes.

Another important tip is to use dynamic styling tools. For example originally I hard-coded my banner element margins for each screen size (lines 214 and 218, 387 and 382, 435-438), but now flexbox does the heavy lifting for me. Dynamic styling will save you lots of time and looks better for less effort.

Step 9 also applies here (don’t copy paste things you don’t understand).

11. Make Sure the Back and Front End are in Sync

My blog will tell you that you were successfully subscribed even if there was an error.

I wanted the process of subscribing to my blog to be contained within a modal window, but I was not sure how to trigger a success message in a modal window without reloading the page (which would close the modal window). My options were to redirect the user to a new page displaying the message or to have Javascript render the message within the modal window as soon as the submit button was clicked without waiting to see if the POST request was actually successful. I went with the second option, but that meant the user would see “Success” even if there was an error. Oops.

12. Make the App Easy to Use

Do yourself a favor and create something that’s easy to use, otherwise you probably won’t use it. For example...

Heroku’s ephemeral filesystem makes creating posts with images impossible using my GUI. Any images I uploaded would be deleted if the dyno is restarted...so I had to find a workaround if I wanted to use the GUI I had so lovingly created:

As you can see in the instructions above, my solution was convoluted and annoying. So much so that I didn’t post on my blog for over a month. Eventually I hope to store images in an S3 bucket, but I have yet to tackle that.

Conclusion

I’ve been called the “Flask master” by a couple of people in my Recurse batch recently. While I am definitely not a master, I hope that you’ll find this post helpful. Thanks for reading!

P.S. Subscribe and I’ll let you know when I add new posts explaining exactly how I fixed some of these issues ;)