Acknowledgments
Thanks to Paul Logston, Tom Repetti, and Ron Lee for their helpful comments on drafts of Learn Enough Python to Be Dangerous. Thanks also to Prof. Jetson Leder-Luis of Boston University and data scientist Amadeo Bellotti for their helpful feedback and assistance in preparing Chapter 11. Any errors that remain in the text are entirely the fault of these fine gentlemen.
About the author
Michael Hartl is the creator of the Ruby on Rails Tutorial, one of the leading introductions to web development, and is cofounder and principal author at Learn Enough. Previously, he was a physics instructor at the California Institute of Technology (Caltech), where he received a Lifetime Achievement Award for Excellence in Teaching. He is a graduate of Harvard College, has a Ph.D. in Physics from Caltech, and is an alumnus of the Y Combinator entrepreneur program.
Learn Enough Python to Be Dangerous
Software Development, Flask Web Apps, and Beginning Data Science with Python
Michael Hartl
Contents
- Acknowledgments
- About the author
- Chapter 1 Hello, world!
- Chapter 2 Strings
- Chapter 3 Lists
- Chapter 4 Other native objects
- Chapter 5 Functions and iterators
- Chapter 6 Functional programming
- Chapter 7 Objects and classes
- Chapter 8 Testing and test-driven development
- Chapter 9 Shell scripts
- Chapter 10 A live web application
- Chapter 11 Data science
Chapter 1 Hello, world!
Welcome to Learn Enough Python to Be Dangerous!
This tutorial is designed to get you started writing practical and modern Python programs as fast as possible, with a focus on the real tools used every day by software developers. You’ll see how everything fits together by learning skills like testing and test-driven development, publishing packages, beginning web development, and data science. As a result, Learn Enough Python to Be Dangerous can serve either as a standalone introduction or as an excellent prerequisite for longer and more syntax-heavy Python tutorials, of which there are many excellent ones.
Python is one of the world’s most popular programming languages, and for good reason. Python has a clean syntax, flexible data types, a wealth of useful libraries, and a powerful and elegant design that supports multiple styles of programming. Python has seen particularly robust adoption for command-line programs (also known as scripting, as discussed in Chapter 9), web development (via frameworks like Flask (Chapter 10) and Django), and data science (especially data analysis using pandas and machine learning with libraries like scikit-learn (Chapter 11)).
Just about the only things Python isn’t good for are running inside a web browser (for which JavaScript is necessary) and writing programs where speed is of the essence. And even in the latter case, specialized libraries like NumPy (Section 11.2) can give us the speed of a lower-level language like C with the power and flexibility of a higher-level language like Python.1
Learn Enough Python to Be Dangerous broadly follows the same structure as Learn Enough JavaScript to Be Dangerous and Learn Enough Ruby to Be Dangerous, either of which can be studied either before or after this tutorial. Because many of the examples are the same, the tutorials reinforce each other nicely—there are few things more instructive in computer programming than seeing the same basic problems solved in two or more different languages.2 As noted in Box 1.1, though, we’ll definitely be writing Python, not JavaScript or Ruby translated into Python.
More so even than users of other languages, Python programmers—sometimes known as Pythonistas—tend to have strongly held opinions on what constitutes proper programming style. For example, as noted by Python contributor Tim Peters in “The Zen of Python” (Section 1.2.1): “There should be one—and preferably only one—obvious way to do it.” (This stands in contrast to a famous principle associated with the Perl programming language known as “TMTOWTDI”: There’s More Than One Way To Do It.)
Code that adheres to good programming practices (as judged by Pythonistas) is known as Pythonic code. This includes proper code formatting (especially the practices in PEP 8 – Style Guide for Python Code), using built-in Python facilities like enumerate()
(Section 3.5) and items()
(Section 4.4.1), and using characteristic idioms like list and dictionary comprehensions (Chapter 6). (As noted in the official documentation, “PEP stands for Python Enhancement Proposal. A PEP is a design document providing information to the Python community, or describing a new feature for Python or its processes or environment.” PEP 8 is the PEP specifically concerned with Python code style and formatting.)
The code in this tutorial generally strives to be as Pythonic as possible given the material introduced at the given point in the exposition. In addition, we will often begin by introducing a series of intentionally unPythonic examples, culminating in a fully Pythonic version. In such cases, the distinction between unPythonic and Pythonic code will be carefully noted.
Pythonistas have been known to be a bit harsh in their judgment of unPythonic code, which can lead beginners to become overly concerned about programming Pythonically. But “Pythonic” is a sliding scale, depending on how much experience you have in the language. Moreover, programming is fundamentally about solving problems, so don’t let worries about programming Pythonically stop you from solving the problems you face in your role as a Python programmer and software developer.
There are no programming prerequisites for Learn Enough Python to Be Dangerous, although it certainly won’t hurt if you’ve programmed before. What is important is that you’ve started developing your technical sophistication (Box 1.2), either on your own or using the preceding Learn Enough tutorials. These tutorials include the following, which together make a good list of prerequisites for this book:
- Learn Enough Command Line to Be Dangerous
- Learn Enough Text Editor to Be Dangerous
- Learn Enough Git to Be Dangerous
All of these tutorials are available for individual purchase, and we offer a subscription service—the Learn Enough All Access subscription—with access to all the corresponding online courses.
An essential aspect of using computers is the ability to figure things out and troubleshoot on your own, a skill we at Learn Enough call technical sophistication.
Developing technical sophistication means not only following systematic tutorials like Learn Enough Python to Be Dangerous, but also knowing when it’s time to break free of a structured presentation and just start Googling around for a solution.
Learn Enough Python to Be Dangerous will give us ample opportunity to practice this essential technical skill.
In particular, as alluded to above, there is a wealth of Python reference material on the Web, but it can be hard to use unless you already basically know what you’re doing. One goal of this tutorial is to be the key that unlocks the documentation. This will include lots of pointers to the official Python site.
Especially as the exposition gets more advanced, I’ll also sometimes include the web searches you could use to figure out how to accomplish the particular task at hand. For example, how do you use Python to manipulate a Document Object Model (DOM)? Like this: python dom manipulation.
You won’t learn everything there is to know about Python in this tutorial—that would take thousands of pages and centuries of effort—but you will learn enough Python to be dangerous (Figure 1.1).3 Let’s take a look at what that means.

In Chapter 1, we’ll begin at the beginning with a series of simple “hello, world” programs using several different techniques, including an introduction to an interactive command-line program for evaluating Python code. In line with the Learn Enough philosophy of always doing things “for real”, even as early as the first chapter we’ll deploy a (very simple) dynamic Python application to the live Web. You’ll also get pointers to the latest setup and installation instructions via Learn Enough Dev Environment to Be Dangerous, which is available for free online and as a free downloadable ebook.
After mastering “hello, world”, we’ll take a tour of some Python objects, including strings (Chapter 2), arrays (Chapter 3), and other native objects (Chapter 4). Taken together, these chapters constitute a gentle introduction to object-oriented programming with Python.
In Chapter 5, we’ll learn the basics of functions, an essential subject for virtually every programming language. We’ll then apply this knowledge to an elegant and powerful style of coding called functional programming (Chapter 6).
Having covered the basics of built-in Python objects, in Chapter 7 we’ll learn how to make objects of our own. In particular, we’ll define an object for a phrase, and then develop a method for determining whether or not the phrase is a palindrome (the same read forward and backward).
Our initial palindrome implementation will be rather rudimentary, but we’ll extend it in Chapter 8 using a powerful technique called test-driven development (TDD). In the process, we’ll learn more about testing generally, as well as how to create and publish a self-contained Python package.
In Chapter 9, we’ll learn how to write nontrivial shell scripts, one of Python’s biggest strengths. Examples include reading from both files and URLs, with a final example showing how to manipulate a downloaded file as if it were an HTML web page.
In Chapter 10, we’ll develop our first full Python web application: a site for detecting palindromes. This will give us a chance to learn about routes, layouts, embedded Python, and form handling. As a capstone to our work, we’ll deploy our palindrome detector to the live Web.
Finally, Chapter 11 introduces several core libraries for doing data science in Python, including NumPy, Matplotlib, pandas, and scikit-learn.
By the way, experienced developers can largely skip the first four chapters of Learn Enough Python to Be Dangerous, as described in Box 1.3.
By keeping a few diffs in mind, experienced developers can skip Chapters 1–4 of this tutorial and start with functions in Chapter 5. They can then move quickly on to functional programming in Chapter 6, consulting earlier chapters as necessary to fill in any gaps.
Here are some of the notable differences between Python and most other languages:
- Use
print
for printing (Section 1.2). - Use
#!/usr/bin/env python3
for the shebang line in shell scripts (Section 1.4). - Single- and double-quoted strings are effectively identical (Section 2.1).
- Use formatted strings (f-strings) and curly braces for string interpolation, e.g.,
f"foo {bar} baz"
for strings"foo"
and"baz"
and variablebar
(Section 2.2). - Use
r"..."
for raw strings (Section 2.2.2). - Python doesn’t have an
obj.length
attribute or anobj.length()
method; instead, uselen(obj)
to calculate object lengths (Section 2.4). - Whitespace is significant (Section 2.4). Lines are typically ended by newlines or colons, and block structure is indicated using indentation (generally four spaces per block level).
- Use
elif
forelse if
(Section 2.4). - In a boolean context, all Python objects are
True
except0
,None
, “empty” objects (""
,[]
,{}
, etc.), andFalse
itself (Section 2.4.2 and later sections). - Use
[...]
for lists (Chapter 3) and{key: value, ...}
for hashes (called dictionaries in Python) (Section 4.4). - Python makes extensive use of namespaces, so importing a library like
math
leads to accessing methods through a library object by default (e.g.,math.sqrt(2)
) (Section 4.1.1).
1.1 Introduction to Python
Created by Dutch developer Guido van Rossum (Figure 1.2),4 Python was originally designed as a high-level, general-purpose programming language. The name Python is a reference, not directly to the snake of that name, but rather to the British comedy troupe Monty Python. This speaks to a certain lightheartedness at the core of Python, but Python is also an elegant, powerful language useful for serious work. Indeed, although I am probably better known for my contributions to the Ruby community (especially the Ruby on Rails Tutorial), Python has long had a special place in my heart (Box 1.4).

Back in the early days of the World Wide Web, I initially learned Perl and PHP for scripting and web development. When I finally got around to learning Python, I was blown away by how much cleaner and more elegant it was than those languages (in my humble opinion and no offense intended). Although I had programmed in a wide variety of languages by that point—including Basic, Pascal, C, C++, IDL, Perl, and PHP—Python was the first language I really loved.
When I was in graduate school, Python played a key role in my doctoral research in theoretical physics, mainly for data processing and as a “glue” language for high-speed simulations written in C and C++. After I graduated, I decided to become an entrepreneur, and I preferred Python so much that I couldn’t bring myself to go back to PHP even though at the time the latter had more mature features for web development. Instead, for my first startup I wrote a custom web framework in Python. (Why not just use Django? This was a while ago, and Django hadn’t been released yet.)
After Ruby on Rails came out, I ended up getting more involved in the Ruby language (eventually leading to the Ruby on Rails Tutorial), but I never lost my interest in Python. I’ve been impressed by how Python’s syntax has continued to mature and become even more elegant, particularly with the advent of Python 3. I was especially pleased to see Python incorporate tau, the mathematical constant I proposed in The Tau Manifesto. Finally, I’ve watched in amazement as Python’s capabilities expanded into areas like numerical computing, plotting, and data analysis (all of which are discussed in Chapter 11), as well as into scientific and mathematical computing (e.g., SciPy and Sage). The power of Python-based systems now genuinely rivals proprietary systems like MATLAB, Maple, and Mathematica; especially given Python’s open-source nature, it seems likely that this trend will continue.
The future looks bright for Python, and I for one expect to use Python frequently in the years to come. As a result, making this tutorial has been a great opportunity for me to reconnect with my Python roots, and I’m glad you’re joining me on the journey.
In order to give you the best broad-range introduction to programming with Python, Learn Enough Python to Be Dangerous uses four main methods:
- An interactive prompt with a Read-Eval-Print Loop (REPL)
- Standalone Python files
- Shell scripts (as introduced in Learn Enough Text Editor to Be Dangerous)
- Python web applications running in a web server
We’ll begin our study of Python with four variations on the time-honored theme of a “hello, world” program, a tradition that dates back to the early days of the C programming language. The main purpose of “hello, world” is to confirm that our system is correctly configured to execute a simple program that prints the string "hello, world!"
(or some close variant) to the screen. By design, the program is simple, allowing us to focus on the challenge of getting the program to run in the first place.
Because one of the most common applications of Python is writing shell scripts for execution at the command line, we’ll start by writing a series of programs to display a greeting in a command-line terminal: first in a REPL; then in a standalone file called hello.py
; and finally in an executable shell script called hello
. We’ll then write (and deploy!) a simple proof-of-concept web application using the Flask web framework (a lightweight framework that serves as good preparation for a heavier framework like Django).
1.1.1 System setup and installation
Throughout what follows, I’ll assume that you have access to a Unix-compatible system like macOS or Linux (including the Linux-based Cloud9 IDE, as described in the free tutorial Learn Enough Dev Environment to Be Dangerous). The cloud IDE is especially well-suited to beginners and is recommended for those looking to streamline their setup process or who run into difficulties configuring their native system.
If you use the cloud IDE, I recommend creating a development environment called python-tutorial
. The cloud IDE uses the Bash shell program by default; Linux and Mac users can use whichever shell program they prefer—this tutorial should work with either Bash or macOS’s default Z shell (Zsh). You can use the following command to figure out which one is running on your system:
$ echo $SHELL
When updating your system settings (as in Section 1.5.1), be sure to use the profile file corresponding to your shell program (.bash_profile
or .zshrc
). See “Using Z Shell on Macs with the Learn Enough Tutorials” for more information.
This tutorial standardizes on Python 3.10, although the vast majority of code will work with any version after 3.7. You can check to see if Python is already installed by running python3 --version
at the command line to get the version number (Listing 1.1).5
$ python3 --version
Python 3.10.6
If instead you get a result like
$ python3 --version
-bash: python3: command not found
or you get a version number earlier than 3.10 then you should install a more recent version of Python.
The details of installing Python vary by system and can require applying a little technical sophistication (Box 1.2). The different possibilities are covered in Learn Enough Dev Environment to Be Dangerous, which you should take a look at now if you don’t already have Python on your system. In particular, if you end up using the cloud IDE recommended by Learn Enough Dev Environment to Be Dangerous, you can update the Python version as shown in Listing 1.2. Note that the steps in Listing 1.2 should work on any Linux system that supports the APT package manager. On macOS systems, Python can be installed using Homebrew as shown in Listing 1.3.
$ sudo add-apt-repository -y ppa:deadsnakes/ppa
$ sudo apt-get install -y python3.10
$ sudo apt-get install -y python3.10-venv
$ sudo ln -sf /usr/bin/python3.10 /usr/bin/python3
$ brew install python@3.10
Whichever way you go, the result should be an executable version of Python (or more specifically, Python 3):
$ python3 --version
Python 3.10.6
(Exact version numbers may differ.)
For historical reasons, many systems include copies of both Python 3 and an earlier version of Python known as Python 2. You can often get away with using the python
command (without the 3
), especially when working in a virtual environment (Section 1.3). As you level up as a Python programmer, you may find yourself using the plain python
command more often, secure in the knowledge that the correct version is being used. This route is more error-prone, though, so we’ll stick with python3
in this tutorial since it makes the version number explicit, with negligible risk of accidentally using Python 2.
1.2 Python in a REPL
Our first example of a “hello, world” program involves a Read-Eval-Print Loop, or REPL (pronounced “repple”). A REPL is a program that reads input, evaluates it, prints out the result (if any), and then loops back to the read step. Most modern programming languages provide a REPL, and Python is no exception; in Python’s case, the REPL is often known as the Python interpreter because it directly executes (or “interprets”) user commands. (A third common term is the Python shell, in analogy with the Bash and Zsh programs used to run command-line shell programs.)
Learning to use the REPL well is a valuable skill for every aspiring Python programmer. As noted Python author David Beazley put it:
Although there are many non-shell environments where you can code Python, you will be a stronger Python programmer if you are able to run, debug, and interact with Python at the terminal [i.e., the REPL]. This is Python’s native environment. If you are able to use Python here, you will be able to use it everywhere else.
The Python REPL can be started with the Python command python3
, so we can run it at the command line as shown in Listing 1.4.
$ python3
>>>
Here >>>
represents a generic Python prompt waiting for input from the user.
We’re now ready to write our first Python program using the print()
command, as seen in Listing 1.5. (Here "hello, world!"
is a string; we’ll start learning more about strings in Chapter 2.)
>>> print("hello, world!")
hello, world!
That’s it! That’s how easy it is to print “hello, world!” interactively with Python.
If you’re familiar with other programming languages (such as PHP or JavaScript), you may have noticed that Listing 1.5 lacks a terminating semicolon to mark the end of the line. Indeed, Python is unusual among programming languages in that its syntax depends on things like newlines (Section 1.2.1) and spaces. We’ll see many more examples of Python’s unique syntax as this tutorial progresses.
1.2.1 Exercises
-
Box 1.1 references “The Zen of Python” by Tim Peters. Confirm that we can print out the full text of “The Zen of Python” using the command
import this
in the Python REPL (Listing 1.6). - What happens if you use
print("hello, world!", end="")
in place ofprint()
by itself? (Theend=""
is known as a keyword argument (Section 5.1.2).) How would you change theend
argument to get the result to match Listing 1.5? Hint: Recall that\n
is the typical way to represent a newline character.
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
1.3 Python in a file
As convenient as it is to be able to explore Python interactively, most Real Programming™ takes place in text files created with a text editor. In this section, we’ll show how to create and execute a Python file with the same “hello, world” program we’ve discussed in Section 1.2. The result will be a simplified prototype of the reusable Python files we’ll start learning about in Section 5.2.
We’ll start by creating a directory for this tutorial and a Python file (with a .py
file extension) for our hello
program (be sure to exit the interpreter first if you’re still in the REPL, which you can do using exit
or Ctrl-D
):
$ cd # Make sure we're in the home directory.
$ mkdir -p repos/python_tutorial
$ cd repos/python_tutorial
Here the -p
option to mkdir
arranges to create intermediate directories if necessary. Note: Throughout this tutorial, if you’re using the cloud IDE recommended in Learn Enough Dev Environment to Be Dangerous, you should replace the home directory ~
with the directory ~/environment
.
Because Python is so widely used, many systems come preinstalled with Python, and default programs often use it extensively. This introduces the possibility of interactions between the version of Python we’re using and the versions used by other programs, and the results can be nasty and confusing. To avoid this headache, one common practice is to use self-contained virtual environments, which allow us to use the exact version of Python we want, and to install whatever Python packages we want, without affecting the rest of the system.
We’ll be using the venv
package combined with pip
to install additional packages. This solution is especially suitable for a tutorial like this one because all of the specifics of the setup are contained in a single directory, which can be deleted and recreated if anything goes wrong. There is another powerful solution called Conda, though, which has a large and enthusiastic following among Python programmers. In my experience, Conda is just a little more difficult to use than venv/pip (e.g., the first time I tried using the conda
utility it took over my system and replaced the default Python, which was tricky to reverse), but as you level up you might find yourself switching over to Conda.6
To create a virtual environment, we’ll use the python3
command with -m
(for “module”) and venv
(the name of the virtual environment module):
$ python3 -m venv venv
Note that the second occurrence of venv
is our choice; we could write python3 -m venv foobar
to create a virtual environment called foobar
, but venv
is the conventional choice. N.B. If you ever completely screw up your Python configuration, you can simply remove the venv directory using rm -rf venv/
and start again (but don’t run that command right now or the rest of the chapter might not work!).
Once the virtual environment is installed, we need to activate it to use it:
$ source venv/bin/activate
(venv) $
Note that many shell programs will insert (venv)
before the prompt $
to remind us that we’re working in a virtual environment. The activate
step is frequently required when using virtual environments, so I suggest creating a shell alias for it, such as va
.7
To deactivate a virtual environment, use the deactivate
command:
(venv) $ deactivate
$
Note that the (venv)
in front of the prompt disappears upon deactivation.
Now let’s reactivate the virtual environment and create a file called hello.py
using the touch
command (as discussed in Learn Enough Command Line to Be Dangerous):
$ source venv/bin/activate
(venv) $ touch hello.py
Next, using our favorite text editor, we’ll fill the file with the contents shown in Listing 1.7. Note that the code is exactly the same as in Listing 1.5, with the difference that in a Python file there’s no command prompt >>>
.
hello.py
print("hello, world!")
At this point, we’re ready to execute our program using the python3
command we used in Listing 1.1 to check the Python version number. The only difference is that this time we omit the --version
option and instead include an argument with the name of our file:
(venv) $ python3 hello.py
hello, world!
As in Listing 1.5, the result is to print “hello, world!” out to the terminal screen, only now it’s the raw shell instead of a Python REPL.
Although this example is simple, it’s a huge step forward, as we’re now in the position to write Python programs much longer than could comfortably fit in an interactive session.
1.3.1 Exercises
- What happens if you give
print()
two arguments, as in Listing 1.8?
hello.py
print("hello, world!", "how's it going?")
1.4 Python in a shell script
Although the code in Section 1.3 is perfectly functional, when writing a program to be executed in the command-line shell it’s often better to use an executable script of the sort discussed in Learn Enough Text Editor to Be Dangerous.
Let’s see how to make an executable script using Python. We’ll start by creating a file called hello
:
(venv) $ touch hello
Note that we didn’t include the .py
extension—this is because the filename itself is the user interface, and there’s no reason to expose the implementation language to the user. Indeed, there’s a reason not to: by using the name hello
, we give ourselves the option to rewrite our script in a different language down the line, without changing the command our program’s users have to type. (Not that it matters in this simple case, but the principle should be clear. We’ll see a more realistic example in Section 9.3.)
There are two steps to writing a working script. The first is to use the same command we’ve seen before (Listing 1.7), preceded by a “shebang” line telling our system to use Python to execute the script.
Ordinarily, the exact shebang line is system-dependent (as seen with Bash in Learn Enough Text Editor to Be Dangerous and with JavaScript in Learn Enough JavaScript to Be Dangerous), but with Python we can ask the shell itself to supply the proper command. The trick is to use the following line to use the python
executable available as part of the shell’s environment (env):
#!/usr/bin/env python3
Using this for the shebang line gives the shell script shown in Listing 1.9.
hello
#!/usr/bin/env python3
print("hello, world!")
We could execute this file directly using the python
command as in Section 1.3, but a true shell script should be executable without the use of an auxiliary program. (That’s what the shebang line is for.) Instead, we’ll follow the second of the two steps mentioned above and make the file itself executable using the chmod
(“change mode”) command combined with +x
(“plus executable”):
(venv) $ chmod +x hello
At this point, the file should be executable, and we can execute it by preceding the command with ./
, which tells our system to look in the current directory (dot = .
) for the executable file. (Putting the hello
script on the PATH, so that it can be called from any directory, is left as an exercise.) The result looks like this:
(venv) $ ./hello
hello, world!
Success! We’ve now written a working Python shell script suitable for extension and elaboration. As mentioned briefly above, we’ll see an example of a real-life utility script in Section 9.3.
Throughout the rest of this tutorial, we’ll mainly use the Python interpreter for initial investigations, but the eventual goal will almost always be to create a file containing Python.
1.4.1 Exercises
- By moving the file or changing your system’s configuration, add the
hello
script to your environment’s PATH. (You may find the steps in Learn Enough Text Editor to Be Dangerous helpful.) Confirm that you can runhello
without prepending./
to the command name. Note: If you have a conflictinghello
program from following Learn Enough JavaScript to Be Dangerous or Learn Enough Ruby to Be Dangerous, I suggest replacing it—thus demonstrating the principle that the file’s name is the user interface, and the implementation can change language without affecting users.
1.5 Python in a web browser
Although it wasn’t initially designed for web development, Python’s elegant and powerful design has led to its widespread use in making web applications. In recognition of this, our final example of a “hello, world” program will be a live web application, written in the simple but powerful Flask micro-framework (Figure 1.3).8 Because of its simplicity, Flask is a perfect introduction to web development with Python while also serving as excellent preparation for a “batteries included” framework like Django.

We’ll begin by installing the Flask package using pip (a recursive acronym that stands for “pip installs packages”). The pip
command comes automatically as part of the virtual environment, so we can access it by typing pip
at the command line (or pip3
on some systems—try the latter if the former doesn’t work). As a first step, it’s a good idea to upgrade pip to ensure we’re running the latest version:
(venv) $ pip install --upgrade pip
Next, install Flask (Listing 1.10).
(venv) $ pip install Flask==2.2.2
We’ve included an exact version number in Listing 1.10 in case future versions of Flask don’t happen to work with this tutorial; this is similar to our decision to use python3
instead of plain python
. As you get more advanced, though, you’ll probably just run things like pip install Flask
, secure in the knowledge that you can figure out what went wrong if the version number doesn’t happen to work.
Believe it or not, the one command in Listing 1.10 installs all of the software needed to run a simple but full-strength web application on our local system (where “local” might refer to the cloud if you’re using the cloud IDE recommended in Learn Enough Dev Environment to Be Dangerous).
Although the code for the “hello, world” web app uses some commands that we haven’t covered yet, it’s a straightforward adaptation of the example program on the Flask home page (Figure 1.4). Being able to adapt code you don’t necessarily understand is a classic hallmark of technical sophistication (Box 1.2).

We’ll put our “hello, world” app in a file called hello_app.py
:
(venv) $ touch hello_app.py
The code itself closely parallels the program in Figure 1.4, as seen in Listing 1.11.
python_tutorial/hello_app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>hello, world!</p>"
The code in Listing 1.11 defines the behavior for the root URL / when responding to an ordinary browser request (known as GET
). The response itself is the required “hello, world!” string, which will be returned to the browser as a (very simple) web page.
To run the web application in Listing 1.11, all we need to do is run the hello_app.py
file using the flask
command (Listing 1.12). (Do make sure you’re running in the virtual environment; weird things can happen if you try running the flask
command on the default system.) In Listing 1.12, the --app
option specifies the app and the --debug
option arranges to update the app when we change the code (which saves us from having to restart the Flask server every time we make a change).
(venv) $ flask --app hello_app.py --debug run
* Running on http://127.0.0.1:5000/
At this point, visiting the given URL (which consists of the local address 127.0.0.1 and the port number) shows the application running on the local machine.9

If you’re using the cloud IDE, the commands are nearly identical to the ones shown in Listing 1.12; the only difference is that you have to include a different port number using the --port
option (Listing 1.13).
(venv) $ flask --app hello_app.py --debug run --port $PORT
* Running on http://127.0.0.1:8080/
To preview the app and replicate the result shown in Figure 1.5, we have to follow a few more steps. First, we need to preview the app as shown in Figure 1.6. The result typically shows up in a small window inside the IDE (details may vary); by clicking the icon shown in Figure 1.7, we can pop out into a new window. The result should appear as in Figure 1.8 (the only difference with Figure 1.5 is the URL).



Just getting a web app to work, even locally, is a huge accomplishment. But the real pièce de résistance is deploying the app to the live Web. This is the goal of Section 1.5.1.
1.5.1 Deployment
Now that we’ve got our app running locally, we’re ready to deploy it to a production environment. This used to be practically impossible to do in a beginning tutorial, but the technology landscape has matured significantly in recent years, to the point where we actually have an abundance of choices. The result will be a production version of the application from Section 1.5.
There’s a bit of overhead involved in deploying something the first time, but deploying early and often is a core part of the Learn Enough philosophy of shipping (Box 1.5). Moreover, a simple app like “hello, world” is the best kind of app for first-time deployment, because there’s so much less that can go wrong.
As legendary Apple cofounder Steve Jobs once said: Real artists ship. What he meant was that, as tempting as it is to privately polish in perpetuity, makers must ship their work—that is, actually finish it and get it out into the world. This can be scary, because shipping means exposing your work not only to fans but also to critics. “What if people don’t like what I’ve made?” Real artists ship.
It’s important to understand that shipping is a separate skill from making. Many makers get good at making things but never learn to ship. To keep this from happening to us, we’ll follow the practice started in Learn Enough Git to Be Dangerous and ship several things in this tutorial. Shipping the “hello, world” app in this section is only the beginning!
As with the GitHub Pages deployment option used in previous tutorials (Learn Enough CSS & Layout to Be Dangerous and Learn Enough JavaScript to Be Dangerous among them), our first step is to put our project under version control with Git (as covered in Learn Enough Git to Be Dangerous). While this is not strictly necessary for the deployment solution used in this section, it’s always a good idea to have a fully versioned project so that we can more easily recover from any errors.
Our first step is to create a .gitignore
file to tell Git to ignore files and directories we don’t want to version. Use touch .gitignore
(or any other method you prefer) to create the file and then fill it with the contents shown in Listing 1.14.10
.gitignore
venv/
*.pyc
__pycache__/
instance/
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
.DS_Store
Next, initialize the repository:
(venv) $ git init
(venv) $ git add -A
(venv) $ git commit -m "Initialize repository"
It’s also a good idea to push any newly initialized repository up to a remote backup. As in previous Learn Enough tutorials, we’ll use GitHub for this purpose (Figure 1.9).

Because web apps sometimes include sensitive information like passwords or API keys, I like to err on the side of caution and use a private repository. Accordingly, be sure to select the Private option when creating the new repository at GitHub, as shown in Figure 1.10. (By the way, it’s still a bad idea to include passwords or API keys, even in a private repo; the best practice is to use environment variables or the like instead.)

Next, tell your local system about the remote repository (taking care to fill in <username>
with your GitHub username) and then push it up:
(venv) $ git remote add origin https://github.com/<username>/python_tutorial.git
(venv) $ git push -u origin main
The service we’ll be using for Flask deployment is Fly.io. We’ll start by installing a necessary package and we’ll then list the requirements (including Flask) needed to deploy the application. Note: The following steps work as of this writing, but deploying to a third-party service is exactly the kind of thing that can change without notice. If that happens, you will likely have an opportunity to apply your technical sophistication (Box 1.2), up to and including finding an alternate service (such as Render) if necessary.
Our first step is to install a package for Gunicorn, a Python web server:
(venv) $ pip install gunicorn==20.1.0
Then we need to create a file called requirements.txt
to tell the deployment host which packages are needed to run our app, which we can do by creating a requirements.txt
file using
$ touch requirements.txt
and then filling it with the contents shown in Listing 1.15, which we can figure out using pip freeze
in a virtual environment where no unneeded packages had been installed. (Some resources recommend redirecting the output of pip freeze
using pip freeze > requirements.txt
to create the file in Listing 1.15, but this approach can lead to unnecessary or invalid packages being required.)
requirements.txt
click==8.1.3
Flask==2.2.2
gunicorn==20.1.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Werkzeug==2.2.2
The current recommended practice for Python package management is to use a pyproject.toml
file to specify the build system for the project. This step is not required when deploying to Fly.io, but we will follow this practice when we make a package of our own in Chapter 8.
With the configuration in Listing 1.15, we have set up our system for Fly.io to detect the presence of a Flask app automatically. Here are the steps for getting started:
- Sign up for Fly.io. Take care to click the link for the free tier, which can be a little tricky to find (Figure 1.11). Free accounts are limited to two deployment servers, which is perfect for us since that’s the number in this tutorial (here and in Chapter 10).
- Install Fly Control (
flyctl
), a command-line program for interacting with Fly.io.11 Options for macOS and for Linux (including the cloud IDE) are shown in Listing 1.16 and Listing 1.17, respectively. For the latter, take care to add any lines to your.bash_profile
or.zshrc
file as instructed (Listing 1.18), and then runsource ~/.bash_profile
(orsource ~/.zshrc
) to update the configuration. Note that the vertical dots in Listing 1.18 indicate omitted lines. - Sign in to Fly.io at the command line (Listing 1.19).12

flyctl
on macOS using Homebrew.
(venv) $ brew install flyctl
flyctl
on Linux.
(venv) $ curl -L https://fly.io/install.sh | sh
flyctl
. ~/.bash_profile or ~/.zshrc
.
.
.
export FLYCTL_INSTALL="/home/ubuntu/.fly"
export PATH="$FLYCTL_INSTALL/bin:$PATH"
(venv) $ flyctl auth login --interactive
Once you’ve signed in to Fly.io, follow these steps to deploy the hello app:
- Run
flyctl launch
(Listing 1.20) and accept the autogenerated name and the default options (i.e., no database). - Edit the generated
Procfile
and fill it with the contents shown in Listing 1.21. You’ll probably have to make only one change by updating the app name fromserver
tohello_app
. - Deploy the application with
flyctl deploy
(Listing 1.22).13
(venv) $ flyctl launch
Procfile
web: gunicorn hello_app:app
(venv) $ flyctl deploy
After the deployment step has finished, you can run the command in Listing 1.23 to see the status of the app. (If anything goes wrong, you may find flyctl logs
helpful in debugging.)
(venv) $ flyctl status # Details will vary
App
Name = restless-sun-9514
Owner = personal
Version = 2
Status = running
Hostname = crimson-shadow-1161.fly.dev # Your URL will differ.
Platform = nomad
Deployment Status
ID = 051e253a-e322-4b2c-96ec-bc2758763328
Version = v2
Status = successful
Description = Deployment completed successfully
Instances = 1 desired, 1 placed, 1 healthy, 0 unhealthy
The highlighted line in Listing 1.23 indicates the URL of the live app, which you can open automatically as follows:
(venv) $ flyctl open # won't work on the cloud IDE, so use displayed URL
As noted, the flyctl open
command won’t work on the cloud IDE because it needs to spawn a new browser window, but in that case you can just copy and paste the URL from your version of Listing 1.23 into your browser’s address bar to obtain the same result.
And that’s it! Our hello app is now running in production (Figure 1.12). “It’s alive!” (Figure 1.13).14


Although there were quite a few steps involved in this section, being able to deploy a site so early is nothing short of miraculous. It may be a simple app, but it’s a real one, and being able to deploy it to production is an enormous step.
By the way, you might have noticed that deploying to Fly.io didn’t require a Git commit (in contrast to, say, GitHub Pages or a hosting service like Heroku). As a result, it’s probably a good idea to make one final commit now and push the result up to GitHub:
(venv) $ git add -A
(venv) $ git commit -m "Configure hello app for deployment"
(venv) $ git push
1.5.2 Exercises
- Change “hello, world!” to “goodbye, world!” in
hello_app.py
running locally. Does the updated text display right away? What about after refreshing the browser? - Deploy your updated app to Fly.io and confirm that the new text appears as expected.