DEV Community

Cover image for Fix “invalid literal for int() with base 10: 'None'” in FastAPI (Asyncpg + Docker)
Rose Wabere
Rose Wabere

Posted on

Fix “invalid literal for int() with base 10: 'None'” in FastAPI (Asyncpg + Docker)

You're using Docker to deploy your FastAPI application with Aiven PostgreSQL, when all of a sudden you notice this traceback:

ValueError: invalid literal for int() with base 10: 'None'
Enter fullscreen mode Exit fullscreen mode

It refers to SQLAlchemy's URL parser and complains that the string "-None" is the port portion of your database URL. Your URL appears as postgresql+asyncpg://user:pass@host:None/dbname. What caused that to occur? And above all, how do you fix it?

In this tutorial, I'll walk you through the cause of this error and provide a clean solution. We'll also discuss best practices for managing environment variables in containerized FastAPI applications.

Understanding the Error

The error occurs when SQLAlchemy's make_url() function tries to parse the database URL and convert the port component to an integer, but receives the literal string "None". This happens because somewhere in your code, you're building the URL with a variable that evaluates to None, and when interpolated into an f-string, None becomes the string "None".

The Code That Breaks

Consider this common pattern in database.py:

import os

DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT")
DB_NAME = os.getenv("DB_NAME")

ASYNC_DB_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
ASYNC_DB_URL = os.getenv("DATABASE_URL")  # attempt to override with full URL

engine = create_async_engine(ASYNC_DB_URL)
Enter fullscreen mode Exit fullscreen mode

When DB_PORT is not set in your environment (such as a Docker container), it becomes None. A URL with :None in the port position is generated by the f-string. The engine creation still uses the final value even if you later reassign ASYNC_DB_URL with the correct DATABASE_URL. Why, then, does it still have "None"? Because the assignment order is deceptive or because DATABASE_URL may also be missing.

In my instance, DATABASE_URL was set, but a string containing "-None" was already created in the first line and used. No, the engine actually makes use of the final value. Since the second line sets ASYNC_DB_URL to None but the engine attempts to use None, the issue must be that DATABASE_URL is not set. That would result in a different error. The port "None" error we observe indicates that the f-string URL is being used.

This only occurs in the event that DATABASE_URL is not set, in which case the code reverts to the constructed URL (possibly through a or clause). For example:

ASYNC_DB_URL = os.getenv("DATABASE_URL") or f"postgresql+asyncpg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
Enter fullscreen mode Exit fullscreen mode

If DATABASE_URL is empty, it uses the constructed URL, which has None for port if any component is missing.

Step-by-Step Debugging

  1. Check your environment variables inside the container:
docker exec <container_name> env
Enter fullscreen mode Exit fullscreen mode

Verify that DATABASE_URL is present and correct, and that individual component variables are not set (unless you intentionally set them).

  1. Inspect the code in database.py. Look for any fallback logic that builds the URL from components. If you see something like the above, that's your culprit.

  2. Simplify: Remove the component-based construction entirely. Use only the full DATABASE_URL:

import os

DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
    raise ValueError("DATABASE_URL environment variable is not set")

engine = create_async_engine(DATABASE_URL)
Enter fullscreen mode Exit fullscreen mode

This ensures you're not accidentally creating a malformed URL.

  1. Test by running your app again. The error should disappear.

Why the Single-Env-Var Approach is Better

  1. Consistency: Most cloud database providers (Aiven, Heroku, etc.) give you a single DATABASE_URL. Using it directly avoids mismatches.
  2. Simplicity: Fewer variables to manage in your .env and Docker configuration.
  3. Portability: Your app can be deployed anywhere without changing how the URL is assembled.
  4. Security: You avoid accidentally logging or exposing partial credentials.

Handling Async with asyncpg

If you're using asyncpg with SQLAlchemy, your DATABASE_URL must use the postgresql+asyncpg:// scheme. Aiven provides a URL like:

postgres://avnadmin:password@host:port/defaultdb?sslmode=require
Enter fullscreen mode Exit fullscreen mode

You need to change the scheme to postgresql+asyncpg://. You can do this programmatically:

raw_url = os.getenv("DATABASE_URL")
if raw_url and raw_url.startswith("postgres://"):
    raw_url = raw_url.replace("postgres://", "postgresql+asyncpg://", 1)
engine = create_async_engine(raw_url)
Enter fullscreen mode Exit fullscreen mode

Full Working Example

Here's a robust database.py for FastAPI with Aiven:

import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base

DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
    raise ValueError("DATABASE_URL environment variable is required")

# Ensure asyncpg scheme
if DATABASE_URL.startswith("postgres://"):
    DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+asyncpg://", 1)

engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
Enter fullscreen mode Exit fullscreen mode

Environment Variables in Docker Compose

In your docker-compose.yml, pass the DATABASE_URL via environment or an env_file:

services:
  app:
    build: .
    env_file:
      - .env
Enter fullscreen mode Exit fullscreen mode

And in your .env file:

DATABASE_URL=postgresql+asyncpg://avnadmin:password@host:port/defaultdb?sslmode=require
Enter fullscreen mode Exit fullscreen mode

Conclusion

The ValueError: invalid literal for int() with base 10: 'None' is a symptom of a configuration mismatch. By adopting a single DATABASE_URL environment variable and eliminating component-based construction, you can avoid this and similar pitfalls. Remember to always validate required environment variables early and use defensive programming.

If you found this helpful, please share it with others who might be struggling with the same issue. And if you have questions, kindly drop them in the comments below!

Top comments (0)