Replace Spring Boot's Conveniences
Spring Boot hands you an IoC container, externalized configuration, bean validation, and Spring Data repositories, mostly through annotations and auto-configuration. Python gives you each capability through a focused library. This page names the stand-in for each one. The next two pages put them to work in a FastAPI service.
Dependency injection
Spring's container scans for beans and wires them through @Autowired. Python has no container, and most code does not need one. You construct objects and pass them in. For request-scoped wiring in a web app, FastAPI provides Depends, which calls a provider function and injects the result into your handler:
from typing import Annotated
from fastapi import Depends
def get_user_service() -> UserService:
return UserService()
UserServiceDep = Annotated[UserService, Depends(get_user_service)]
A handler that declares service: UserServiceDep receives a UserService. The provider replaces @Bean and @Autowired. Page 5 uses this pattern in a route.
| Spring Boot | Python |
|---|---|
@Autowired, constructor injection |
pass the object in, or Depends in a web handler |
@Bean |
a provider function |
@Component, @Service |
a plain class |
| IoC container | no container, explicit construction |
Configuration
Spring Boot reads application.yml and binds it to @ConfigurationProperties classes, with profiles per environment. The Python counterpart is pydantic-settings (uv add pydantic-settings), which reads environment variables and a .env file into a typed, validated settings object:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
request_timeout: int = 30
settings = Settings()
A missing database_url fails at startup with a clear error, the way a missing required property stops a Spring context from loading. @Value("${request.timeout}") becomes a typed field with a default.
Validation
Spring uses Bean Validation (@Valid, @NotNull, @Size) on request DTOs. Python uses pydantic, where the type annotations are the validation and Field adds constraints:
from pydantic import BaseModel, EmailStr, Field
class CreateUser(BaseModel):
email: EmailStr
display_name: str = Field(min_length=1, max_length=80)
age: int = Field(ge=0)
Construct CreateUser(**payload) and pydantic coerces and validates, raising ValidationError on bad input. EmailStr needs the validator extra (uv add "pydantic[email]"). FastAPI runs this validation for you on every request body, covered on page 5.
| Bean Validation | pydantic |
|---|---|
@NotNull |
a non-optional field |
@Size(min=1, max=80) |
Field(min_length=1, max_length=80) |
@Min(0) |
Field(ge=0) |
@Email |
EmailStr |
@Valid on a controller argument |
automatic on a typed FastAPI body |
Persistence
Spring Data JPA gives you @Entity classes and repository interfaces it implements at runtime. The Python counterpart is SQLAlchemy 2.0. You declare mapped classes the way you annotate entities:
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True)
display_name: Mapped[str] = mapped_column()
active: Mapped[bool] = mapped_column(default=True)
One difference matters. Spring Data generates repository implementations from interface method names like findByEmail. SQLAlchemy gives you a Session and you write the query, or wrap it in a small repository class of your own:
from sqlalchemy import select
def find_by_email(session, email: str) -> User | None:
return session.scalars(select(User).where(User.email == email)).first()
| Spring Data JPA | SQLAlchemy 2.0 |
|---|---|
@Entity |
a Base subclass with Mapped columns |
@Id @GeneratedValue |
mapped_column(primary_key=True) |
JpaRepository<User, Long> |
a Session plus select(...), or your own repo class |
findByEmail(...) |
session.scalars(select(User).where(...)) |
spring.jpa.hibernate.ddl-auto |
Base.metadata.create_all(engine) |
Page 6 wires a session into the request lifecycle and runs a query end to end.