The technical debt of language loyalty We often treat programming languages like sports teams, wearing our Python or Rust badges with a sense of tribal pride. But tying your professional identity to a specific syntax creates a dangerous blind spot. When you define yourself as a "Python Developer," any criticism of the language feels like a personal attack. This defensive posture stops you from evaluating tools objectively and prevents you from seeing where a project actually needs a different approach. Seniority isn't about how many dunder methods you know; it's about the ability to choose the right tool for the specific job without an ideological filter. Living with Python's inherent flaws Let's be honest: Python has real problems. Semantic whitespace can feel fragile to those raised on C++ or Java. The performance lag is undeniable when compared to Go, and the packaging ecosystem has historically been a fragmented mess of virtual environments and requirements files. Even with modern improvements like uv, the underlying inconsistencies in class design and the verbosity of type hints remain. Many of the reasons to dislike the language are just as true today as they were a decade ago. Outcomes over ideology Despite these flaws, Python remains a powerhouse because of its utility in the "glue code" that runs the modern world. If 90% of your work involves orchestrating APIs, database queries, and LLM integrations, the bottleneck is almost never execution speed—it's network latency. The massive ecosystem and ease of setup make it the pragmatic choice for machine learning and automation. Actionable steps for the senior mindset Start by de-coupling your worth from your stack. Practice "tool-agnostic" design by focusing on architecture that could theoretically be implemented in any language. Don't judge other developers for their choices, as you rarely know their specific team constraints or existing infrastructure requirements. Finally, give yourself permission to use multiple languages across different projects. You don't have to marry your tools; you just need them to solve the problem at hand.
uv
Products
ArjanCodes (9 mentions) highlights uv for dependency management and modern Python development, citing its role in scalable projects, as seen in videos like "The Best Way to Share Code Between Python Apps" and "UV Just Got A Serious Upgrade."
- Apr 3, 2026
- Oct 24, 2025
- Oct 3, 2025
- Aug 8, 2025
- Jul 4, 2025
Overview Serverless functions allow you to run code in the cloud without managing the underlying infrastructure. Google Cloud Functions (now part of Cloud Run Functions) scale automatically based on demand, including scaling to zero when not in use. This model enables developers to focus purely on business logic while the cloud provider handles provisioning and runtime maintenance. This guide explores building these functions using the Functions Framework, managing them through Infrastructure as Code (IaC) with Pulumi, and navigating technical trade-offs. Prerequisites To follow this guide, you should have a basic understanding of Python and HTTP request handling. Familiarity with Google Cloud Platform (GCP) and terminal-based package management is recommended. You will need a GCP account with billing enabled to deploy resources. Key Libraries & Tools - **Functions Framework**: A Google-provided open-source library that lets you run your functions locally to simulate the cloud environment. - **Pulumi**: An infrastructure-as-code tool that uses standard programming languages to define cloud resources. - **uv**: A high-performance Python package installer and resolver used to manage project dependencies. - **Flask**: The underlying web framework used by Google Cloud Functions to handle routing. Code Walkthrough 1. Local Development You can create a function by defining a simple Python routine. The Functions Framework allows you to serve this locally for testing. ```python import functions_framework @functions_framework.http def hello_world(request): return "Hello, Serverless!" ``` Run this locally using the command: `functions-framework --target hello_world --port 8080`. This provides an immediate feedback loop before cloud deployment. 2. Infrastructure as Code with Pulumi To deploy, use Pulumi to define the necessary infrastructure. You must create a storage bucket to hold the source code artifact and then define the function resource itself. ```python import pulumi import pulumi_gcp as gcp Create a bucket for the code bucket = gcp.storage.Bucket("function-bucket") Define the cloud function cloud_fn = gcp.cloudfunctionsv2.Function("my-function", build_config=gcp.cloudfunctionsv2.FunctionBuildConfigArgs( runtime="python311", entry_point="hello_world", ), service_config=gcp.cloudfunctionsv2.FunctionServiceConfigArgs( max_instance_count=10, available_memory="256M", timeout_seconds=60, )) ``` 3. Simulating Sub-routes Since Google Cloud Functions typically only expose a single entry point, you can use an internal Flask app to handle complex routing within that single function. ```python from flask import Flask, request internal_app = Flask(__name__) @internal_app.route("/user/<id>") def get_user(id): return {"user_id": id} @functions_framework.http def entry_point(request): with internal_app.test_request_context(path=request.path, method=request.method): return internal_app.dispatch_request() ``` Syntax Notes When using Pulumi, you define resources as Python objects. Note the `service_config` block, which dictates how the cloud provider allocates resources. Google Cloud Functions specifically look for a `requirements.txt` file for dependency resolution; if you use uv, you must export your lockfile to this format during the build process. Practical Examples Serverless functions excel at event-driven tasks. Common use cases include processing an invoice whenever a PDF is uploaded to a storage bucket, sending a welcome email via an HTTP trigger during user registration, or performing data validation on a Pub/Sub message stream. Tips & Gotchas - **Cold Starts**: If a function hasn't been called recently, the first request may be slow as the provider spins up a new container instance. Keep dependencies light to minimize this delay. - **Security**: Never hardcode API keys. Use Google Cloud Secret Manager to store sensitive values and inject them as environment variables into your function. - **Access Control**: Be careful with the `allUsers` invoker role; it makes your function public, which could lead to unexpected costs if abused by third parties.
Jun 20, 2025Overview Managing source code effectively is the difference between a streamlined release and a chaotic debugging session. This guide explores the mechanical and strategic nuances of Git branching. By using a FastAPI web application as a concrete example, we demonstrate how to isolate new features, maintain a clean history, and choose between different integration strategies like merging and rebasing. Understanding these patterns allows you to collaborate without stepping on your teammates' toes. Prerequisites To follow this tutorial, you should have a baseline understanding of Python syntax and the basic concept of version control. You will need a terminal environment, Git installed, and a package manager like UV or pip. Familiarity with basic HTTP methods (GET) and unit testing with Pytest is also beneficial. Key Libraries & Tools * **FastAPI**: A modern, fast (high-performance) web framework for building APIs with Python. * **GitKraken**: A visual Git client that simplifies branch management and history visualization. * **UV**: An extremely fast Python package installer and resolver. * **Pytest**: A framework that makes it easy to write small, readable tests. Code Walkthrough Initializing the API We start by defining a simple root endpoint. This serves as our stable baseline on the `main` branch. ```python from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} ``` Isolating Features via Branches Instead of modifying `main` directly, create a feature branch. This keeps the production-ready code clean while you experiment. We add a new `goodbye` endpoint and a corresponding unit test to verify it works. ```python @app.get("/goodbye") def say_goodbye(name: str = "World"): return {"message": f"Goodbye {name}"} ``` Merging vs. Rebasing When it is time to bring changes back to `main`, you face a choice. A **Standard Merge** creates a new commit that ties the two histories together. This preserves the exact context of when a feature was developed but can lead to a "spaghetti" visual history. **Rebasing** offers a cleaner alternative. It takes your feature commits, sets them aside, moves your branch to the tip of the current `main`, and then reapplies your work on top. This results in a perfectly linear history. If `main` hasn't changed since you branched, Git performs a **Fast-Forward**, simply moving the branch pointer forward without creating a new commit at all. Syntax Notes * **Feature Flags**: When using Trunk-Based Development, use boolean constants to toggle code paths. This allows you to merge unfinished code safely. * **Naming Conventions**: In GitFlow, prefix branches with `feature/` or `hotfix/` to organize the repository automatically. Practical Examples Real-world teams often use **GitFlow** for structured releases where separate `develop` and `main` branches exist. Alternatively, fast-moving startups might prefer **Trunk-Based Development**, pushing directly to `main` while hiding incomplete features behind logic toggles to avoid long-lived branch conflicts. Tips & Gotchas * **Rewrite History with Caution**: Never rebase a branch that others are also working on. It changes commit hashes and will break their local environments. * **Small Commits**: Commit early and often. Smaller commits make resolving merge conflicts significantly easier. * **Test Before Integration**: Always run Pytest on your feature branch before merging to ensure you aren't introducing regressions.
Apr 11, 2025Overview Effective testing separates amateur scripts from professional software. Pytest transforms the grueling chore of verification into a streamlined, automated workflow. By adopting a minimalist approach, you reduce the friction of writing tests, ensuring your codebase remains resilient as it scales. This guide focuses on building a clean, manageable testing environment that mirrors your project structure and utilizes modern tooling. Prerequisites To follow this tutorial, you should have a solid grasp of **Python** fundamentals, specifically modules and functions. Familiarity with the command line and a code editor like VS Code is essential. Knowledge of virtual environments will help you manage dependencies without polluting your system Python installation. Key Libraries & Tools * **Pytest**: The primary framework for writing and running small, readable tests. * **uv**: A high-performance Python package installer and resolver. * **Pytest-mock**: A plugin that simplifies mocking objects and patching imports. * **Poetry**: An alternative dependency management tool for Python projects. Code Walkthrough 1. Installation Isolate your testing tools by installing them as development dependencies. Using the `uv` package manager ensures speed and reliability. ```bash uv add --test dev pytest ``` 2. VS Code Configuration To enable the visual testing interface in VS Code, you must explicitly configure your workspace settings. This allows the editor to discover tests within your designated directory. ```json { "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, "python.testing.pytestArgs": [ "test" ] } ``` 3. Writing Unit Tests Test files must follow the `test_*.py` naming convention. Match your `test/` folder structure to your `src/` folder for easy navigation. Below is a test for a duration conversion function. ```python from datetime import timedelta from src.timestamp_utils import to_timestamp def test_negative_duration(): duration = timedelta(seconds=-10) assert to_timestamp(duration) == "0:00:00" def test_specific_time(): duration = timedelta(hours=1, minutes=2, seconds=3) assert to_timestamp(duration) == "1:02:03" ``` Syntax Notes Pytest relies on standard Python `assert` statements rather than the verbose helper methods found in the legacy `unittest` library. Functions must start with the `test_` prefix to be automatically discovered by the runner. For more complex scenarios, **Mocking** replaces real objects with fake ones to isolate the logic under test. Practical Examples Testing is vital for external API integrations, such as OpenAI. Instead of making real network calls during testing, you can **patch** the translator function to return a static string. This keeps your test suite fast and avoids unnecessary costs. Tips & Gotchas * **Filter by Keyword**: Use `pytest -k "keyword"` to run specific subsets of tests. * **Reset State**: Always ensure one test does not depend on the outcome of another; isolation is king. * **Check Performance**: Use the `--durations=10` flag to identify the slowest tests in your suite.
Mar 21, 2025Overview Most developers begin their containerization journey with a bloated Dockerfile that results in massive images and sluggish deployment pipelines. An unoptimized image often exceeds a gigabyte, dragging down CI/CD performance and increasing cloud storage costs. By moving away from heavy generic OS images like Ubuntu and adopting modern tooling, you can reduce image sizes by over 80% while significantly hardening security. Prerequisites To follow this guide, you should have a baseline understanding of Python development and Docker fundamentals. Familiarity with the terminal, basic shell commands, and the concept of virtual environments will help you navigate the more advanced multi-stage build patterns. Key Libraries & Tools - FastAPI: A high-performance web framework for building APIs with Python. - Poetry: A tool for dependency management and packaging in Python. - uv: An extremely fast Python package installer and resolver written in Rust. - Debian: The Linux distribution serving as the foundation for most official Python images. Refined Base Images and Tagging Standard Python images often include unnecessary build tools and headers. Switching to `python:3.13-slim-bookworm` provides a minimal Debian base without the bloat. Furthermore, avoid the `latest` tag; it is a rolling target that introduces unpredictability. Specific tags ensure your production environment remains consistent and reproducible. The Multi-Stage Build Pattern This technique separates the build environment from the runtime environment. You use a heavy image with compilers to install dependencies, then copy only the resulting virtual environment into a slim production image. ```dockerfile Stage 1: Builder FROM python:3.13-bookworm AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv WORKDIR /app COPY pyproject.toml . RUN uv venv && uv sync Stage 2: Runtime FROM python:3.13-slim-bookworm WORKDIR /app COPY --from=builder /app/.venv /.venv ENV PATH="/.venv/bin:$PATH" COPY ./src ./src CMD ["uvicorn", "src.main:app"] ``` Syntax Notes and Performance Using `apt-get install --no-install-recommends` prevents the installation of suggested packages that aren't strictly required. Additionally, chaining `rm -rf /var/lib/apt/lists/*` in the same `RUN` layer clears out the package index metadata, which serves no purpose in a final production image. Practical Examples In a real-world FastAPI deployment, applying these steps took an image from nearly 1GB down to roughly 150MB. By using uv instead of Poetry inside the Dockerfile, build times dropped from 36 seconds to under 9 seconds due to faster dependency resolution. Tips & Gotchas Always run your container as a non-root user to prevent attackers from gaining system-level access. Use `RUN useradd -m appuser && USER appuser`. A common mistake is copying the entire project directory (`COPY . .`), which can accidentally leak `.env` files or include heavy local artifacts like `node_modules` or `__pycache__`.
Mar 14, 2025Overview uv represents a paradigm shift in Python tooling. Developed by Astral in Rust, it acts as a high-performance replacement for `pip`, `poetry`, `pyenv`, and `virtualenv`. The primary advantage is speed; uv resolves and installs packages significantly faster than legacy tools while providing a unified interface for managing Python versions and virtual environments. Prerequisites To follow this guide, you should have a basic understanding of the Python ecosystem, including how to use the terminal and the purpose of a `pyproject.toml` file. While no specific version of Python is required to start—since uv can install Python for you—having a shell environment like Zsh or Bash is necessary. Key Libraries & Tools * **uv**: An extremely fast Python package and project manager. * **Ruff**: An extremely fast Python linter and formatter, also by Astral. * **Homebrew**: A macOS package manager used for easy installation. * **Cargo**: The Rust package manager, used if building uv from source. Code Walkthrough Installation On macOS, install via Homebrew: ```bash brew install uv ``` Alternatively, use a standalone script for any OS: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` Project Initialization Create a new project structure with a standard `pyproject.toml` and a managed virtual environment: ```bash uv init my-project cd my-project ``` This command generates a boilerplate Git setup, a `.python-version` file, and a basic `hello.py` script. Managing Dependencies Add and remove packages seamlessly. uv automatically updates your requirements and syncs the environment: ```bash uv add pandas fast-api uv remove sql-alchemy ``` Executing Code Run scripts directly within the context of your managed environment without manually activating it: ```bash uv run hello.py ``` Syntax Notes uv uses a command structure reminiscent of Cargo or `npm`. The `uvx` command (shorthand for `uv tool run`) allows for one-off execution of CLI tools like Ruff without permanently adding them to your project dependencies. Practical Examples In monorepo environments, uv supports **Workspaces**. This allows multiple projects to share a single lockfile and virtual environment, reducing disk usage and ensuring version consistency across different microservices or internal libraries. Tips & Gotchas * **Shell Completion**: Enable tab-completion for faster terminal navigation by running `uv generate-shell-completion zsh` and adding it to your config. * **Build Systems**: Currently, uv relies on backends like `hatchling` for building packages. It does not yet include a built-in Rust-based build backend, though this is actively being developed. * **Python Versions**: Use `uv python install 3.13` to manage runtimes without needing pyenv.
Dec 13, 2024Overview: Unmasking the FastAPI Engine In an unscripted deep-dive into the internals of FastAPI, we move beyond the high-level marketing of "performance" and "developer experience" to examine the actual plumbing. FastAPI has exploded in popularity over the last few years, often cited as the modern successor to Django and Flask. However, a tactical look at the source code reveals a library that is less an independent framework and more a sophisticated, highly documented orchestration layer. The session explores the project's heavy reliance on Starlette for its ASGI server capabilities and Pydantic for data validation, questioning whether the architectural glue holding these pieces together is as elegant as the public-facing API suggests. Key Strategic Decisions: The Inheritance Gamble The most striking structural choice in the FastAPI codebase is its aggressive use of inheritance. In many core modules, such as `applications.py` and `exceptions.py`, FastAPI types are direct subclasses of Starlette components. While this allows for rapid development and full access to the underlying toolkit, it creates a tight coupling that makes the framework inherently brittle. By inheriting rather than using composition, FastAPI exposes the entire Starlette API to its users, essentially merging the two interfaces. This decision prioritizes speed of implementation over encapsulation. If Starlette introduces a breaking change, FastAPI has no buffer to shield its users, necessitating immediate and potentially complex refactors across the entire ecosystem. Performance Breakdown: Optimization vs. Abstraction When developers ask why FastAPI is faster than Django, the answer doesn't lie in revolutionary Python code within the FastAPI repo itself. The performance is largely inherited from Starlette, which is built on top of high-performance ASGI standards. FastAPI adds a layer of Pydantic for serialization and typing-extensions for metadata. This combination is powerful but comes with a massive amount of "boilerplate city" within the source. For example, the `Param` functions and `APIReady` classes involve deeply nested arguments and repetitive initializers. While this provides the end-user with a clean, typed interface, the internal maintenance cost is high. The framework spends a significant amount of its internal logic simply passing variables down long inheritance chains to satisfy the requirements of its dependencies. Critical Moments: The Documentation Paradox A critical moment in the analysis occurs when examining the `routing.py` and `applications.py` files. These files are massive, but not necessarily because of logic density. A vast percentage of the code consists of docstrings and metadata used to generate Swagger UI and ReDoc. This is the "Documentation Paradox": FastAPI's greatest feature for users—automatic, interactive docs—is also its greatest source of internal clutter. The code often lacks sufficient documentation for *developers* of the framework, even as it overflows with documentation for *users* of the framework. This creates a readability hurdle for anyone looking to contribute to the core, as the actual logic is frequently buried under hundreds of lines of string literals and type definitions. Refactoring Opportunities: Moving Toward Composition To improve the long-term health of the project, a shift away from inheritance toward composition would be a superior tactical move. By making Starlette an internal object within FastAPI rather than a parent class, the developers could create a cleaner boundary. This would allow for a more functional approach where data flows through clearly defined transformations rather than being stored in a massive `self` object with dozens of attributes. Implementing the Strategy Pattern for response serialization—replacing long `isinstance` checks with a mapping dictionary—would also significantly reduce the cyclomatic complexity of the routing logic. These changes would make the framework less brittle and easier to extend without breaking the existing public API. Future Implications: The Maintenance Burden of Compatibility The current source code reflects a heavy maintenance burden regarding Pydantic V1 and V2 compatibility. The presence of logic branches that handle different versions of validation libraries highlights the difficulty of managing a popular open-source project. While supporting older versions of Python and dependencies is noble for adoption, it leads to a fragmented codebase. Looking ahead, the framework must eventually shed this legacy weight. The eventual transition to uv for package management and a stricter adherence to modern Python 3.10+ features (like native generics) will be necessary to keep the codebase from becoming an unmanageable legacy system. Developers should appreciate FastAPI for the tool it is—a brilliant user interface—while remaining critical of the architectural shortcuts taken under the hood.
Dec 4, 2024Overview Setting up a Python development environment in VSCode often feels like a constant battle against broken imports, mismatched interpreter versions, and testing suites that refuse to discover your code. This guide moves past temporary fixes to establish a robust, professional workflow. We will focus on creating a project structure that Pylance understands and Pytest can navigate, using modern tools like UV for dependency management. Prerequisites To follow along, you should have VSCode installed and a basic understanding of the Python language. Familiarity with the terminal or command prompt is necessary for running installation commands and project initialization. Key Libraries & Tools * **UV**: A fast Python package installer and resolver written in Rust, used as a modern alternative to Poetry. * **Ruff**: An extremely fast Python linter and code formatter. * **Pylance**: The default language server for Python in VSCode, providing IntelliSense and type checking. * **Pytest**: A framework that makes it easy to write simple and scalable test suites. * **Even Better TOML**: An extension for better syntax highlighting and navigation in configuration files. Project Structure and Initialization Instead of installing packages globally, we use UV to create an isolated environment. Start by initializing your project in the terminal: ```bash uv init --no-workspace ``` A professional structure separates the logic from the metadata. Move your code into a `src` directory and include a `__init__.py` file to signal that it is a package. Your directory should look like this: ```text my_project/ └── src/ └── my_app/ └── __init__.py └── main.py └── tests/ └── test_main.py └── pyproject.toml ``` To add Pytest as a development dependency, run: ```bash uv add --dev pytest ``` Solving the Import and Test Discovery Crisis The most common headache is Pytest failing to find your modules because it doesn't know about the `src` folder. You fix this by adding a `pythonpath` setting to your `pyproject.toml` file: ```toml [tool.pytest.ini_options] pythonpath = "src" ``` However, Pylance might still show "reportMissingImports" in the editor even if tests run. You must align the editor's analysis with your runtime path by creating a `.vscode/settings.json` file: ```json { "python.analysis.extraPaths": ["./src"], "python.testing.pytestArgs": ["tests"], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true } ``` Professional VSCode Configuration Managing settings across a team requires moving beyond global user settings. Use **Folder Settings** within the `.vscode` directory to ensure every developer on the project uses the same interpreter and formatter. Recommended Extensions Share a consistent toolset by creating `.vscode/extensions.json`. When a team member opens the project, VSCode will prompt them to install the necessary tools: ```json { "recommendations": [ "ms-python.python", "charliermarsh.ruff", "tamasfe.even-better-toml" ] } ``` Syntax Notes and Conventions * **TOML Folding**: Use the Even Better TOML extension to collapse large configuration blocks in your `pyproject.toml`. * **Bundled Formatters**: If you use the Ruff extension, enable the `useBundled` setting to avoid needing a separate local installation of the binary. * **Analysis Paths**: Always use relative paths (like `./src`) in `extraPaths` to ensure settings work across different machines. Tips & Gotchas * **Priority of Settings**: Remember that **Folder Settings** override **Workspace Settings**, which in turn override **User Settings**. If your project isn't behaving, check the local `.vscode/settings.json` first. * **Syncing**: Use VSCode's built-in Settings Sync for personal preferences like themes and fonts, but keep project-specific logic (like the Python path) in the repository. * **Source Folder**: Never name your root code folder `source` or `src` without an `__init__.py` if you intend to import it as a package; Pylance needs that marker to recognize the package boundary correctly.
Nov 22, 2024Overview Modern API development often feels like a redundant exercise in data modeling. Traditionally, developers building with FastAPI find themselves writing two distinct sets of models: SQLAlchemy classes for the database schema and Pydantic models for request validation and response serialization. This "double definition" creates a maintenance burden where every change must be mirrored across both systems. SQLModel solves this by merging these two worlds, allowing you to define a single class that serves as both a database table and a data validation model. Prerequisites To follow this guide, you should be comfortable with Python (3.10+ recommended), basic REST API concepts, and the fundamentals of SQL. Familiarity with asynchronous programming and type hints will help you understand the framework's internal logic. Key Libraries & Tools - **SQLModel**: The star of the show. It sits on top of SQLAlchemy and Pydantic to unify data schemas. - **FastAPI**: The high-performance web framework used to build our API endpoints. - **Uvicorn**: An ASGI server implementation to run our application. - **uv**: A high-speed Python package installer and resolver used to manage the project environment. Code Walkthrough Converting a legacy SQLAlchemy setup to SQLModel drastically reduces the surface area of your code. Let's look at how we define a Hero model that acts as both our table and our schema. ```python from typing import Optional from sqlmodel import Field, SQLModel, create_engine, Session, select class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True, index=True) name: str secret_name: str age: Optional[int] = None ``` In this snippet, `table=True` tells SQLModel to create a database table for this class. Note how we use standard Python type hints. The `Field` function allows us to inject database-specific constraints like `primary_key` without losing Pydantic's validation power. To interact with the database in a FastAPI route, the session management becomes much cleaner: ```python @app.post("/heroes/", response_model=Hero) def create_hero(hero: Hero, session: Session = Depends(get_session)): session.add(hero) session.commit() session.refresh(hero) return hero ``` We no longer need to map a Pydantic object to a separate SQLAlchemy object. The `hero` object passed into the function is already compatible with the database session. Relationship Management SQLModel handles complex relationships, such as many-to-many links, using a dedicated Link model. This prevents data redundancy by storing associations in a join table while keeping the object-oriented interface clean. ```python class HeroMissionLink(SQLModel, table=True): hero_id: Optional[int] = Field(default=None, foreign_key="hero.id", primary_key=True) mission_id: Optional[int] = Field(default=None, foreign_key="mission.id", primary_key=True) ``` Tips & Gotchas While unifying models is efficient, it introduces a risk of **tight coupling**. If your database model is exactly the same as your API response, you might accidentally leak sensitive fields like hashed passwords or internal IDs. To prevent this, use inheritance: create a `HeroBase` for shared fields, then extend it into a `Hero` (with `table=True`) and a `HeroPublic` (for safe API responses). This gives you the best of both worlds: reduced boilerplate with explicit security boundaries.
Nov 15, 2024