- 👤 Andrés Cruz

🇪🇸 En español

The Ultimate Python Development Guide: From Zero to REST API with FastAPI

Python has consolidated its position as one of the most influential and fastest-growing programming languages in the world. Its clean syntax, vast standard library, and an ecosystem that spans from web development and data science to automation and artificial intelligence, make it an indispensable tool for any developer. 

In this SUPER post, I offer you a complete journey that will take you from the fundamentals of the language and setting up a professional development environment, to building high-performance web APIs with FastAPI, one of the most modern and efficient frameworks in the ecosystem. We will cover the initial setup, virtual environment management, the principles of API development, data validation with Pydantic, authentication, testing, and finally, deploying your application to the cloud.

Get ready to master the tools and techniques that define modern Python development. Whether you are starting your programming journey or looking to modernize your backend development skills, this guide will provide you with the practical and detailed knowledge you need to succeed.

Section 1: Getting Started with Python

Before building complex applications, mastering the fundamentals is crucial. This section will guide you through installing Python, configuring your development environment, and understanding why virtual environments are a non-negotiable practice for any professional project.

Why Python? The Language that Dominates Modern Development

Python is an interpreted, cross-platform, and object-oriented programming language that has experienced massive growth in recent years. Its popularity is not a coincidence; it is due to a combination of simplicity, power, and versatility.

An Ecosystem for Everything

Python is not limited to a single domain. Its flexibility allows you to build:

More than 127,000 Libraries

The Python Package Index (PyPI) hosts a massive repository of libraries that you can install and use in your projects with a simple command line (pip install). This means you will rarely have to "reinvent the wheel."

Simplicity and Productivity

Python was designed to be readable and expressive. It allows developers to write less code to achieve the same results as in other languages, which translates into faster development and easier maintenance.

If you are looking for a language to learn that is in demand, powerful, and applicable to almost any problem you can imagine, Python is, without a doubt, the right choice.

Learn more about the initial installation and configuration in our article: Getting started with Python: Why Python?, installation and configuration.

Essential Fundamentals to Start with Python

Although this guide is not an introduction to programming from scratch, a review of the basic tools and Python concepts is essential before diving into web development.

The Python Interactive Console

Once you have Python installed and configured in your PATH, you can access its interactive console by simply typing python in your terminal. You will see the prompt, $\gg>$, which indicates it is ready to receive commands. This console is an invaluable tool for testing small code snippets, exploring an object's functionality, or performing quick calculations without having to create a .py file.

Variables and Data Types

Python is a dynamically typed language, which means you do not need to declare a variable's type. The interpreter infers it at runtime.

# Text strings (string) 
my_string = "Hello, World" 
another_string = 'Also with single quotes' 
# Integers (int) 
my_integer = 10 
# Floating point numbers (float) 
my_float = 3.1416 # Booleans (bool) 
is_true = True is_false = False 
# Lists (list) - mutable and ordered 
my_list = [1, "two", 3.0, True] 
# Tuples (tuple) - immutable and ordered my_tuple = (1, "two", 3.0) 
# Dictionaries (dict) - key-value pairs 
my_dictionary = {"name": "Andres", "age": 30}

Control Structures

As in other languages, Python uses if, elif, and else for conditionals, and for and while for loops. Indentation is syntactically significant; it defines code blocks.

# Conditionals 
if mi_entero > 5:
    print("Es mayor que 5")
elif mi_entero == 5:
    print("Es igual a 5")
else:
    print("Es menor que 5")
# For loop
for elemento in mi_lista:
    print(elemento)
# While loop
contador = 0
while contador < 3:
    print(f"Contador: {contador}")
    contador += 1

Functions

Functions are defined with the def keyword. They can accept arguments and return values.

def greet(name): 
   return f"Hello, {name}!" 
   
   message = greet("DesarrolloLibre") 
   print(message) # Prints "Hello, DesarrolloLibre!"

These are the building blocks upon which we will construct much more complex applications.

For a more detailed review, visit: Everything you need to know to start with Python.

Virtual Environments: The Indispensable Professional Tool

When I started developing with Python, I encountered a very common problem: "dependency hell." Project A needed version 1.2 of a library, while Project B required version 2.0 of the same one. Installing one broke the other. The solution to this chaos is virtual environments.

A virtual environment is a tool that creates an isolated directory containing a Python installation and all the libraries and dependencies your project needs, separate from the global Python installation and other projects.

Why are they so important?

Creating and Activating a Virtual Environment with venv

venv is the standard Python module for creating virtual environments. The process is simple:

# 1. Navigate to your project folder 
cd mi-proyecto-python 
# 2. Create the virtual environment (commonly called 'venv') 
# On Windows python -m venv venv 
# On macOS/Linux python3 -m venv venv 
# 3. Activate the virtual environment 
# On Windows (PowerShell) .\venv\Scripts\Activate.ps1 # On macOS/Linux source venv/bin/activate

Once activated, your terminal prompt will change, usually prefixed with (venv), indicating that any package you install with pip will be installed within this isolated environment.

Managing Dependencies with requirements.txt

Within an activated environment, you can install your project's dependencies. It is good practice to list them in a requirements.txt file.

cd mi-proyecto-python
# In Windows
python -m venv venv
# In macOS/Linux
python3 -m venv venv
# In Windows (PowerShell)
.\venv\Scripts\Activate.ps1
# In macOS/Linux
source venv/bin/activate

Never start a Python project without first creating and activating a virtual environment. It is the foundation of a professional and reproducible workflow.

Master environment creation and project cloning in our guides: Create virtual environments in Python and Clone a GitHub project and create your virtual environment.

Section 2: Building High-Performance APIs with FastAPI

Once the fundamentals of Python and environment management are mastered, it is time to build something for the web. While Django is a "batteries included" framework, microframeworks like Flask and FastAPI offer a lighter, more flexible approach, ideal for creating APIs. In this section, we will dive fully into FastAPI, a modern framework that stands out for its incredible performance and ease of use.

Why FastAPI? A Comparison with Flask

Both Flask and FastAPI are microframeworks, meaning they provide a minimalist core and give you the freedom to choose the tools and libraries you want to use. However, FastAPI introduces several key advantages that make it especially suitable for modern API development.

Flask: Simplicity and Maturity

Flask has long been the microframework of choice. It is extremely simple to learn, flexible, and has a large community and a vast collection of extensions. It is an excellent and solid option, especially for traditional web applications.

FastAPI: Performance and Modern Features

FastAPI, although newer, is built on two pillars that give it a competitive advantage:

  1. Asynchronous Performance: FastAPI is built on Starlette and Uvicorn, which allows it to handle requests asynchronously (using async and await). This translates into performance on par with Node.js and Go frameworks, ideal for applications with high concurrency.
  2. Data Validation with Pydantic: FastAPI uses Python's type hints and the Pydantic library to automatically validate, serialize, and deserialize data. This not only drastically reduces repetitive code but also prevents a large number of errors.
  3. Automatic and Interactive Documentation: Based on type hints and Pydantic validation, FastAPI automatically generates interactive documentation for your API (using the OpenAPI and Swagger UI standards). You can test your endpoints directly from the browser, which is an immense advantage for development and for the consumers of your API.

In summary, while Flask is a fantastic and versatile option, FastAPI's native features for asynchronous development, data validation, and auto-documentation make it the superior choice for building robust, high-performance RESTful APIs in Python today.

Compare both frameworks in detail at: Why Learn to Develop in Flask and/or FastApi.

"Hello World" in FastAPI: From Installation to the First Endpoint

Getting started with FastAPI is surprisingly quick. Following best practices, we will do it within a virtual environment.

1. Installing Dependencies

We need two main packages: fastapi and an ASGI server like uvicorn to run our application.

# Activate your virtual environment first 
$ pip install fastapi uvicorn[standard]

2. Creating the First Endpoint

Create a file named api.py with the following content:

from fastapi import FastAPI
# 1. Create an instance of the FastAPI application
app = FastAPI()
# 2. Define a "route decorator" for the root endpoint ("/") with the GET method
@app.get("/")
def hello_world():
    # 3. The function that is executed when the route is accessed
    # FastAPI automatically converts Python dictionaries to JSON
    return {"hello": "world"}

Let's analyze the code:

3. Running the Development Server

From the terminal, in the same folder where api.py is located, run the Uvicorn server:

$ uvicorn api:app --reload

Now, if you open your browser and go to http://127.0.0.1:8000, you will see the JSON response: {"hello":"world"}.

The Magic of Automatic Documentation

But that is not all. FastAPI has already generated the documentation for you. Go to:

From Swagger UI, you can view your endpoints, their parameters, their responses, and test them live! This is the power of FastAPI from the very first minute.

Follow the initial steps in our guides: Package Installation and Hello World in FastAPI.

Data Validation with Pydantic

One of FastAPI's most powerful features is its native integration with Pydantic for data validation. Pydantic uses Python's type hints to define data models, and FastAPI uses them to:

Migrating from Pydantic V1 to V2: Key Concepts

With the arrival of Pydantic V2, rewritten in Rust for even greater performance, some syntax changed. Understanding these changes is understanding how Pydantic works today.

We define our data models by creating classes that inherit from pydantic.BaseModel.

from pydantic import BaseModel
class Task(BaseModel):
    id: int
    name: str
    description: str | None = None # An optional field

Changes in Model Configuration

Previously, model configuration was done in a nested subclass called Config. In Pydantic V2, it was renamed to ConfigDict, and the values are defined in a dictionary.

A crucial change was orm_mode, which allowed Pydantic to create models from ORM objects like SQLAlchemy. It is now called from_attributes.

# Pydantic V1
class Task(BaseModel):
    ...
    class Config:
        orm_mode = True
# Pydantic V2
from pydantic import ConfigDict
class Task(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    ...

Another change was schema_extra, used to provide example data for documentation. It is now called json_schema_extra.

# Pydantic V1
class Config:
    schema_extra = {"example": {"name": "Mi Tarea", "description": "To Do"}}
# Pydantic V2
model_config = ConfigDict(
    json_schema_extra={"examples": [{"name": "Mi Tarea", "description": "To Do"}]}
)

When using these models in your endpoints, FastAPI does the magic for you:

@app.post("/tasks/")
async def create_task(task: Task):
    # 'task' aquí es una instancia de la clase Task de Pydantic.
    # FastAPI ya ha validado que el JSON de la petición
    # cumple con la estructura y tipos definidos.
    return task

If you send a JSON without the required name field or with an id that is not an integer, FastAPI will automatically return a 422 Unprocessable Entity error with details of the problem.

Learn the details of the migration and best practices at: Migrating a FastAPI application from Pydantic V1 to V2 and Example Data of the FastAPI Request.

Rendering HTML with Jinja2

Although FastAPI is optimized for APIs, it can also serve HTML pages, making it a viable alternative to Flask or Django for traditional web applications. The most common way to do this is using the Jinja2 template engine.

Jinja2 Configuration

First, install Jinja2:

$ pip install jinja2

Then, in your application, create an instance of Jinja2Templates, specifying the directory where you will store your HTML templates.

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
# 1. Create an instance of the FastAPI application
app = FastAPI()
# 2. Initialize Jinja2Templates, pointing to the 'templates' directory
# (You must create a directory named 'templates' and place 'index.html' inside it)
templates = Jinja2Templates(directory="templates")
# 3. Define the route decorator for the root endpoint ("/") with the GET method
# It now requires the Request object to render a template
@app.get("/")
async def read_root(request: Request):
    # The 'context' dictionary contains the data passed to the template
    context = {
        "request": request,
        "title": "FastAPI Web Guide",
        "items": ["Apple", "Pear", "Orange"]
    }
    # 4. Use TemplateResponse to render 'index.html' with the context data
    return templates.TemplateResponse("index.html", context)

The request object is mandatory in the context. TemplateResponse will render the index.html template with the provided data.

Jinja2 Template Syntax

Create a templates folder and an index.html file inside it.

<!DOCTYPE html>
<html>
<head>
    <title>{{ titulo }}</title> <!-- Imprime una variable -->
</head>
<body>
    <h1>{{ titulo }}</h1>
    
    <ul>
        {% for item in items %}<!-- Estructura de control: bucle for -->
            <li>{{ item }}</li>
        {% endfor %}
    </ul>
    {% if items|length > 2 %}<!-- Condicional y filtro -->
        <p>¡Hay más de dos ítems!</p>
    {% endif %}
</body>
</html>

Jinja2 uses:

This separation of logic (Python) and presentation (HTML) is a fundamental practice in web development.

Learn to integrate Jinja2 into your project at: First steps with Jinja and FastAPI.

Authentication and Security with Tokens

Rarely will an API be completely open. Protecting endpoints is crucial. A common and robust method is token-based authentication, such as OAuth2 with Bearer Tokens.

FastAPI provides security tools that integrate perfectly with automatic documentation.

Basic Security Scheme

We can define a security scheme that tells FastAPI (and Swagger documentation) that we expect a token in the Authorization header.

from typing import Dict, Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
# --- Configuration ---
# 'tokenUrl' is the path where the client can request a new token (e.g., login endpoint)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# --- Application Instance ---
app = FastAPI()
# --- Mock Data (Replace with real database/logic) ---
# A very simple example mapping token to user data
fake_users_db: Dict[str, Dict[str, str]] = {
    "fake-super-secret-token": {"username": "johndoe", "full_name": "John Doe"},
    "another-token": {"username": "janedoe", "full_name": "Jane Doe"},
}
# --- Dependency Function (Token Validation) ---
async def get_current_user(token: str = Depends(oauth2_scheme)) -> Dict[str, str]:
    """
    Validates the OAuth2 token provided in the Authorization header.
    :param token: The token extracted from the 'Authorization: Bearer <token>' header.
    :raises HTTPException: If the token is invalid or the user is not found.
    :return: The user dictionary if validation is successful.
    """
    # 1. Decode and Validate the token (e.g., check JWT signature, expiry)
    #    In a real app, you would verify the token's validity here.
    # 2. Look up the user in the database
    user: Optional[Dict[str, str]] = fake_users_db.get(token)
    # 3. Handle invalid token/user not found
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            # This header tells the client *how* to authenticate
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user
# --- Protected Endpoint ---
@app.get("/users/me", response_model=Dict[str, str])
async def read_users_me(current_user: Dict[str, str] = Depends(get_current_user)):
    """
    Retrieves the currently authenticated user's information.
    
    The 'Depends(get_current_user)' ensures this function only runs
    if the token is valid and a user is found.
    """
    # If the

The key is Depends(get_current_user). FastAPI will execute this "dependency" function before calling the route function. The dependency validates the token and returns the user data. If the validation fails, the dependency raises an HTTP exception and the route execution stops.

Token Generation (Login Endpoint)

You will need an endpoint (e.g., /token or /login) where the user sends their username and password to obtain a valid token.

from fastapi.security import OAuth2PasswordRequestForm
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    # 1. Validate username and password (form_data.username, form_data.password)
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    
    # 2. Create the access token
    access_token = create_access_token(data={"sub": user["username"]})
    
    return {"access_token": access_token, "token_type": "bearer"}

OAuth2PasswordRequestForm is a dependency that parses the request body expecting username and password. The create_access_token function would generate the token (usually a JSON Web Token - JWT).

This integration not only protects your API but also adds an "Authorize" interface in Swagger UI, allowing developers to authenticate and easily test the protected endpoints.

Implement a complete authentication system at: Authentication using Tokens in FastAPI.

Unit Testing with Pytest and TestClient

Automated tests are essential to ensure your API works as expected and to prevent regressions when you add new features. FastAPI integrates perfectly with Pytest, and provides a TestClient class to make requests to your API in a testing environment.

Test Environment Setup

First, install pytest and requests (necessary for TestClient).

$ pip install pytest requests

Writing Tests

Create a test_api.py file. TestClient works like the requests library but calls your FastAPI application directly.

from fastapi.testclient import TestClient
from .api import app # Importa tu instancia de FastAPI
client = TestClient(app)
def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"hello": "world"}
def test_create_task_success():
    response = client.post(
        "/tasks/",
        json={"id": 1, "name": "Tarea de prueba"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Tarea de prueba"
    assert data["description"] is None
def test_create_task_invalid_data():
    # Probar un request sin el campo 'name' requerido
    response = client.post(
        "/tasks/",
        json={"id": 2, "description": "Falta el nombre"},
    )
    # FastAPI/Pydantic debería devolver un error 422
    assert response.status_code == 422

To run the tests, simply run pytest in your terminal.

pytest

pytest will automatically discover and execute files and functions that follow its naming conventions (test_*.py files, test_* functions).

With TestClient, you can simulate requests to all your endpoints, including headers, JSON bodies, and query parameters, and make assertions about the status code and the response, ensuring the reliability of your API.

Learn to configure your test suite at: Testing in FastAPI with Pytest.

Section 3: Advanced Concepts and Deployment

Once you have built and tested your application, the next steps are to understand deeper concepts of the language and bring your creation to the real world. This section covers an advanced topic of object-oriented programming in Python and guides you through the deployment process on a modern cloud platform.

Multiple Inheritance and the Method Resolution Order (MRO)

Python is one of the few popular languages that natively supports multiple inheritance. This means that a class can inherit attributes and methods from more than one parent class, allowing for a very powerful composition of behaviors (similar to mixins or traits in other languages).

class Reader:
    """
    A base class representing an entity that can read.
    """
    def read(self) -> None:
        """Prints a message indicating a read operation is occurring."""
        print("Reading...")
class Writer:
    """
    A base class representing an entity that can write.
    """
    def write(self) -> None:
        """Prints a message indicating a write operation is occurring."""
        print("Writing...")
class ReaderWriter(Reader, Writer):
    """
    A class that inherits capabilities from both Reader and Writer.
    This demonstrates multiple inheritance in Python.
    """
    # This class is empty, as it automatically inherits all methods 
    # from its parent classes (Reader and Writer).
    pass
# --- Usage ---
my_object = ReaderWriter()
# Method inherited from Reader
my_object.read()
# Method inherited from Writer
my_object.write()

The "Diamond Problem" and the MRO

Multiple inheritance introduces a potential problem: what happens if multiple parent classes have a method with the same name? Which one executes? This is known as the "diamond problem."

Python resolves this ambiguity using a deterministic algorithm called C3 linearization, which generates a Method Resolution Order (MRO). The MRO is an ordered list of the classes that will be consulted to find a method. You can always inspect a class's MRO using the __mro__ attribute.

print(LectorEscritor.__mro__)
# Salida: 
# (<class '__main__.LectorEscritor'>, 
#  <class '__main__.Lector'>, 
#  <class '__main__.Escritor'>, 
#  <class 'object'>)

This tells us that if we call a method on ReaderWriter, Python will look for it first in ReaderWriter, then in Reader, then in Writer, and finally in the base class object.

The Case of the __init__ Constructor

The MRO is especially important when calling parent class constructors using super().__init__(). In a chain of multiple inheritance, super() does not necessarily call the __init__ of the direct parent class, but the __init__ of the *next class in the MRO*.

This can be confusing. Sometimes, to have explicit control over which constructor is called, it is clearer to call it directly by the name of the parent class, passing self explicitly.

class BasePayment(PaymentPaypalClient, PaymentStripeClient):
    def __init__(self):
        # Instead of super(), which would follow the MRO...
        # super().__init__()
        
        # We explicitly call the constructor that interests us
        PaymentPaypalClient.__init__(self)

Understanding the MRO is key to debugging unexpected behaviors in complex class hierarchies and to using multiple inheritance effectively.

Explore a practical case of this concept in a payment system at: Multiple inheritance in Python and the MRO.

Deploying Python Applications with Railway

Writing code is only half the job. For the world to use your application, you need to deploy it to a server. Platforms like Railway have greatly simplified this process, offering an experience similar to the late free tier of Heroku.

Railway is a cloud deployment platform that takes your source code (usually from a GitHub repository), detects the language and framework, and automatically builds and deploys your application into a container.

Key Features of Railway

Typical Deployment Process

  1. Prepare your application: Make sure you have a requirements.txt file. For a FastAPI application, you'll also need a Procfile, a text file that tells the platform how to run your application.
# Procfile web: uvicorn api.main:app --host 0.0.0.0 --port $PORT

$PORT is an environment variable that Railway will dynamically provide.

  1. Create a project in Railway: Log in, create a new project, and choose "Deploy from GitHub repo".
  2. Select your Repository: Railway will analyze it and begin the build process.
  3. Configure Environment Variables: Add any API keys or configuration your application needs.
  4. Ready! (Done!) Railway will build your application in a Docker container, deploy it, and give you a URL to access it.

Platforms like Railway remove much of the complexity of systems administration (DevOps), allowing developers to focus on what they do best: writing code.

Discover how to deploy your projects step-by-step at: Railway, deploy your applications like on Heroku for FREE.

Conclusion: The Modern Python Ecosystem

We have completed an exhaustive journey through modern development with Python. We started with the foundations, understanding why Python is so dominant and how to set up a professional working environment with virtual environments. Then, we dove headfirst into building high-performance APIs with FastAPI, covering everything from the first "Hello World" to crucial topics like data validation, token-based authentication, and the importance of automated testing.

Finally, we explored deeper language concepts like multiple inheritance and closed the development cycle by learning to deploy our application to the cloud with platforms like Railway. This journey demonstrates that Python, far from being just a scripting language, is a complete and robust ecosystem, capable of taking an idea from conception to a global, secure, and scalable application.

Software development is a constantly evolving field. The tools we have explored here—FastAPI, Pydantic, Pytest—represent the state of the art in backend development with Python. We encourage you to keep exploring, building, and delving into these concepts. The power and elegance of Python are at your disposal to create the next great application.

Ver Listado »