BaseModel
BaseModel turns an ordinary Python class into a data-validation and data-parsing class.
This is especially important in FastAPI, where API requests and responses are usually
represented using Pydantic models.
1. First, what problem is Pydantic solving?
Suppose your application receives this data:
{
"name": "Akhilesh",
"age": 27,
"email": "akhilesh@gmail.com"
}
You could simply use a Python dictionary:
user = {
"name": "Akhilesh",
"age": 27,
"email": "akhilesh@gmail.com"
}
But Python doesn't automatically enforce anything.
For example:
user = {
"name": "Akhilesh",
"age": "hello",
"email": 123
}
Python is perfectly happy with this.
Your application, however, probably isn't.
You might want:
name
→ string
age
→ integer
email → string
This is where Pydantic comes in.
2. What is BaseModel?
You write:
from pydantic import BaseModel
Then:
class User(BaseModel):
name: str
age: int
email: str
Now User is not just an ordinary Python class.
Because it inherits from BaseModel, Pydantic provides functionality such as:
data validation
type checking
data parsing
conversion of compatible input types
error reporting
serialization
JSON conversion
schema generation
You can then do:
user = User(
name="Akhilesh",
age=27,
email="akhilesh@gmail.com"
)
And access the values normally:
print(user.name)
print(user.age)
Output:
Akhilesh
27
3. Why do we need to inherit BaseModel?
This is the most important part of your question.
You might ask:
Why can't I just create my own class?
For example:
class User:
name: str
age: int
email: str
The answer is:
You can.
There is nothing stopping you.
But this class doesn't automatically get Pydantic's functionality.
For example:
class User:
name: str
age: int
email: str
Now:
user = User()
Python doesn't automatically say:
"Hey, age should be an integer."
It doesn't perform Pydantic validation.
4. Think of BaseModel as a machine you
inherit
Imagine Pydantic has already written a huge amount of code:
class BaseModel:
# validation code
# parsing code
# serialization code
# error handling
# JSON handling
# schema generation
# etc.
You don't want to write all of that yourself.
So you do:
class User(BaseModel):
name: str
age: int
This means:
"Create a User model using all the capabilities that Pydantic's BaseModel provides."
This is ordinary Python inheritance.
5. Simple inheritance example without
Pydantic
Let's forget Pydantic for a moment.
Suppose:
class Animal:
def eat(self):
print("Eating")
Then:
class Dog(Animal):
pass
Because Dog inherits from Animal:
dog = Dog()
dog.eat()
works.
You didn't write eat() inside Dog.
It came from Animal.
Similarly:
class User(BaseModel):
name: str
age: int
User inherits functionality from:
BaseModel
6. What does Pydantic's BaseModel give us?
This is where it becomes useful.
Consider:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
Now:
user = User(
name="Akhilesh",
age=27
)
Pydantic validates the data.
Example: wrong data
Suppose:
user = User(
name="Akhilesh",
age="hello"
)
Pydantic will reject it because:
age
is expected to be an integer.
You get a validation error rather than silently accepting invalid data.
7. Why is this particularly useful for APIs?
Suppose you have a FastAPI endpoint.
A client sends:
{
"name": "Akhilesh",
"age": 27
}
You can define:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int
@app.post("/users")
def create_user(user: User):
return user
Now FastAPI knows:
POST /users
Expected body:
{
"name": string,
"age": integer
}
The Pydantic model becomes the contract for the API.
8. What happens when a request arrives?
Suppose the client sends:
{
"name": "Akhilesh",
"age": 27
}
FastAPI roughly does this conceptually:
HTTP Request
↓
JSON data
↓
Pydantic User model
↓
Validate data
↓
Create User object
↓
Your function
Your function receives:
user: User
rather than some random dictionary.
So you can do:
@app.post("/users")
def create_user(user: User):
print(user.name)
print(user.age)
return {
"message": f"Hello {user.name}"
}
9. What if the client sends invalid data?
Suppose:
{
"name": "Akhilesh",
"age": "abc"
}
Your endpoint doesn't need to manually write:
if not isinstance(age, int):
...
Pydantic performs validation.
FastAPI can return a structured validation error.
That's one of the major reasons Pydantic is so useful in API development.
10. What about dictionaries?
You might ask:
Why not just use dictionaries and validate manually?
You could.
For example:
def create_user(data):
if "name" not in data:
raise ValueError("name required")
if "age" not in data:
raise ValueError("age required")
if not isinstance(data["age"], int):
raise ValueError("age must be integer")
This works.
But imagine you have:
User
Address
Order
Product
Payment
Customer
Employee
MedicalStudy
SDTMRecord
You would repeatedly write validation logic.
Pydantic eliminates a lot of this repetitive code.
11. Field comes into the picture here
You mentioned:
from pydantic import BaseModel, Field
BaseModel defines the model.
Field allows you to add additional rules/metadata to individual fields.
For example:
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(gt=0)
Here:
age: int
says:
age must be an integer.
While:
Field(gt=0)
says:
age must be greater than 0.
So:
User(name="Akhilesh", age=27)
works.
But:
User(name="Akhilesh", age=-5)
fails validation.
12. More useful Field examples
You can do:
class User(BaseModel):
name: str = Field(min_length=3)
age: int = Field(gt=0, lt=120)
Now:
name
↓
minimum 3 characters
age
↓
greater than 0
less than 120
For example:
User(name="Ak", age=27)
will fail because:
"Ak"
has only 2 characters.
13. Field can also provide descriptions
This becomes particularly useful with FastAPI.
class User(BaseModel):
name: str = Field(
description="User's full name"
)
age: int = Field(
description="User's age",
gt=0
)
FastAPI can use this information to generate API documentation.
So Field isn't just validation.
It can also provide metadata.
14. Required vs optional fields
Consider:
class User(BaseModel):
name: str
age: int
Both are required.
So this:
User(name="Akhilesh")
will fail because age is missing.
You can provide a default:
class User(BaseModel):
name: str
age: int = 18
Now:
user = User(name="Akhilesh")
works.
Pydantic creates:
{
"name": "Akhilesh",
"age": 18
}
15. Optional fields
You can also use:
from typing import Optional
class User(BaseModel):
name: str
age: Optional[int] = None
This means age can be:
integer
OR
None
For example:
User(name="Akhilesh")
works.
And:
User(name="Akhilesh", age=27)
also works.
16. Pydantic can parse data
One very useful feature is that Pydantic doesn't just validate existing Python objects.
It can also parse external data.
For example:
data = {
"name": "Akhilesh",
"age": 27
}
user = User.model_validate(data)
Now:
print(user.name)
gives:
Akhilesh
17. Converting a model back to a dictionary
You can do:
user.model_dump()
Result:
{
"name": "Akhilesh",
"age": 27
}
And JSON:
user.model_dump_json()
This is very useful when sending data to another service/API.
18. Why not use dataclass?
This is an excellent question.
Python already has:
from dataclasses import dataclass
You could write:
@dataclass
class User:
name: str
age: int
So why Pydantic?
Because a dataclass primarily provides convenient object construction/storage.
Pydantic is designed around:
Data
↓
Parsing
↓
Validation
↓
Structured model
↓
Serialization
For example, Pydantic provides much richer validation behavior.
That's why Pydantic is heavily used for:
APIs
configuration
data ingestion
LLM applications
structured outputs
FastAPI
data validation
19. Very important distinction: type hints vs
validation
This confuses many Python beginners.
Look at:
class User:
age: int
The:
:int
is a type annotation.
It does NOT automatically mean Python will enforce the type at runtime.
Python allows:
user.age = "hello"
Type hints mainly help:
developers
IDEs
static type checkers
documentation
Pydantic takes those annotations and uses them as part of runtime data validation/parsing.
So think:
Python type annotation
↓
"What type should this be?"
Pydantic
↓
"Let's actually validate/parse the incoming data according to that model."
20. Why inheritance specifically?
You could imagine Pydantic having a function:
create_model(...)
instead of inheritance.
In fact, Pydantic does have ways to dynamically create models.
But for normal application code, inheritance gives you a very clean syntax:
class User(BaseModel):
name: str
age: int
You are effectively saying:
"This is a Pydantic model, and these are its fields."
The inheritance also means the class gets the methods and behavior implemented by BaseModel.
Conceptually:
BaseModel
│
┌─────────┼─────────┐
↓
↓
↓
User
Product
Order
Each model gets Pydantic's capabilities.
21. Think about BaseModel like a framework
contract
This is probably the easiest mental model.
When you write:
class User(BaseModel):
you are telling Pydantic/FastAPI:
"Treat this class as a structured data model."
If you write:
class User:
you're simply telling Python:
"This is an ordinary Python class."
That's the fundamental difference.
22. A realistic FastAPI example
Imagine your RAG application has an endpoint:
POST /query
The client sends:
{
"question": "What is SDTM?",
"top_k": 5
}
You can define:
from pydantic import BaseModel, Field
class RAGRequest(BaseModel):
question: str = Field(
min_length=1,
description="Question asked by the user"
)
top_k: int = Field(
default=5,
gt=0,
le=20,
description="Number of documents to retrieve"
)
Then:
@app.post("/query")
def query(request: RAGRequest):
print(request.question)
print(request.top_k)
return {
"question": request.question,
"top_k": request.top_k
}
Now your API has a clear contract:
RAGRequest
│
├── question → string, required
│
└── top_k → integer
default = 5
> 0
<= 20
23. Without Pydantic
You would potentially need:
@app.post("/query")
def query(request):
if "question" not in request:
raise ValueError("question required")
if not isinstance(request["question"], str):
raise ValueError("question must be string")
if "top_k" in request:
if not isinstance(request["top_k"], int):
raise ValueError("top_k must be integer")
if request["top_k"] <= 0:
raise ValueError("top_k must be > 0")
if request["top_k"] > 20:
raise ValueError("top_k must be <= 20")
...
With Pydantic:
class RAGRequest(BaseModel):
question: str = Field(min_length=1)
top_k: int = Field(default=5, gt=0, le=20)
Much cleaner.
24. Pydantic is especially important for your
AI/RAG work
You'll encounter Pydantic constantly in modern AI applications.
For example, an LLM might be expected to produce:
{
"answer": "SDTM is...",
"confidence": 0.92,
"sources": [
"SDTMIG.pdf"
]
}
You can define:
class RAGResponse(BaseModel):
answer: str
confidence: float
sources: list[str]
Now your application has a defined structure for the LLM output.
This is extremely useful because LLMs naturally produce text, while your application often
needs structured data.
25. One more important concept: schema
Your Pydantic class:
class User(BaseModel):
name: str
age: int
implicitly describes a schema:
User
│
├── name: string
└── age: integer
Pydantic can generate JSON Schema from this model.
For example:
print(User.model_json_schema())
You get a machine-readable description of the model.
This is one reason FastAPI and Pydantic work so well together.
FastAPI can use the Pydantic model to generate OpenAPI documentation.
26. The whole picture
Think of the layers like this:
Your application
│
↓
┌─────────────┐
│ RAGRequest
│
│
│
│ question
│
│ top_k
│
└──────┬──────┘
│
↓
┌─────────────┐
│
Pydantic
│
│ BaseModel
│
└──────┬──────┘
│
validation / parsing
│
↓
Valid Python object
│
↓
Your business
logic
27. BaseModel vs Field
Keep this distinction very clear:
BaseModel
Defines:
"This class is a Pydantic data model."
Example:
class User(BaseModel):
...
Field
Defines additional rules/metadata for a particular field.
Example:
age: int = Field(gt=0)
So:
class User(BaseModel):
name: str = Field(min_length=3)
age: int = Field(gt=0)
means:
BaseModel
↓
Makes User a Pydantic model
name: str
↓
name must be a string
Field(min_length=3)
↓
name must have at least 3 characters
age: int
↓
age must be an integer
Field(gt=0)
↓
age must be > 0
28. The most important mental model
When you see:
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(min_length=3)
age: int = Field(gt=0)
read it in your head as:
"Create a structured Pydantic data model called User. It contains a string called name that
must have at least 3 characters, and an integer called age that must be greater than zero."
And when you see:
user: User
think:
"This variable is expected to contain a validated User object."
Final comparison
dict
Manual
JSON/schema
support
Manual Manual
Normal
class
Manual
Manual Manual
Approach
Validation
Parsing
dataclass
Limited/manual Limited Manual
BaseModel Built-in
Built-in Built-in
So the key reason for:
FastAPI
integration
Basic
Limited
Possible
Excellent
class User(BaseModel):
is not that Python requires you to inherit BaseModel.
It is because Pydantic needs your class to inherit from BaseModel so it can give that class its
data-validation, parsing, serialization, and schema capabilities.
If you're learning FastAPI, the next useful step is understanding exactly what happens to a
JSON request from the moment it enters FastAPI → Pydantic validation → BaseModel
object → your endpoint function.
How Pydantic is different
from Dataclass in python?
Yes — Python's dataclass and Pydantic's BaseModel look very similar, so this is a very good
question.
The simplest answer is:
dataclass is mainly for conveniently creating Python objects.
Pydantic BaseModel is mainly for validating and parsing external data.
This distinction becomes especially important in FastAPI and AI/RAG applications.
1. First, they look almost the same
Dataclass
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
Pydantic
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
At first glance:
"Why do I need Pydantic? Python already has dataclasses."
Exactly.
The difference becomes clear when bad or external data enters your program.
2. Dataclass does NOT validate types
Consider:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
Now:
user = User(
name="Akhilesh",
age="hello"
)
You might expect Python to say:
ERROR: age must be an integer
But it doesn't.
The object is created.
print(user)
You get something like:
User(name='Akhilesh', age='hello')
Why?
Because:
age: int
is primarily a type annotation.
The dataclass doesn't automatically enforce it at runtime.
3. Pydantic does validation
Now:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
Try:
user = User(
name="Akhilesh",
age="hello"
)
Pydantic raises a validation error.
It essentially says:
age
Input should be a valid integer
That's a major difference.
4. But there's something interesting
Pydantic can also convert compatible input.
For example:
user = User(
name="Akhilesh",
age="27"
)
Depending on Pydantic's validation rules/configuration, "27" can be parsed into:
27
So:
print(user.age)
can give:
27
and:
type(user.age)
is:
<class 'int'>
So Pydantic isn't merely saying:
"Is this correct?"
It can also say:
"Can I safely turn this input into the type you requested?"
That's the parsing part.
5. Why does this matter in real applications?
Imagine you wrote:
def calculate_discount(user):
return user.age * 2
With a dataclass, someone could accidentally create:
User(
name="Akhilesh",
age="hello"
)
Then later:
user.age * 2
causes problems.
With Pydantic, invalid data can be rejected at the boundary before it enters your application
logic.
This is a very important design principle:
External/untrusted data
↓
VALIDATION
↓
Internal application
Pydantic is particularly good at that boundary.
6. The biggest difference: validation rules
Suppose you want:
age must:
be an integer
be greater than 18
be less than 100
With Pydantic:
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(gt=18, lt=100)
Now:
User(name="Akhilesh", age=27)
works.
But:
User(name="Akhilesh", age=15)
fails.
And:
User(name="Akhilesh", age=150)
fails.
The validation rule is declared right next to the field.
7. Dataclass can validate — but you have to
build it yourself
You might say:
"Can't I do validation with dataclass?"
Yes!
For example:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
def __post_init__(self):
if self.age <= 18:
raise ValueError("Age must be greater than 18")
Now validation exists.
But notice what happened.
You had to write it yourself.
And if you have:
name
age
email
phone
address
salary
country
you potentially have to write lots of validation code.
Pydantic provides a validation framework for this.
8. Email is a great example
Suppose your API receives:
{
"name": "Akhilesh",
"email": "hello"
}
You want the email to be valid.
With Pydantic, you can use specialized types/constraints.
For example:
from pydantic import BaseModel
from pydantic import EmailStr
class User(BaseModel):
name: str
email: EmailStr
Now Pydantic validates the email field.
You don't have to manually implement email validation.
9. Dataclass is excellent for internal Python
data
Imagine your application internally calculates coordinates:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
Then:
p = Point(10.5, 20.3)
This is perfectly reasonable.
You control the data.
It's already inside your Python application.
You don't necessarily need Pydantic.
10. Pydantic is excellent for external data
Imagine an API receives:
{
"name": "Akhilesh",
"age": 27
}
This data came from:
Browser
↓
Internet
↓
Your API
You don't control what the client sends.
They could send:
{
"name": 123,
"age": "hello"
}
or:
{
"age": -500
}
or:
{}
You need validation.
That's where Pydantic shines.
11. This is why FastAPI loves Pydantic
Consider:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int
@app.post("/users")
def create_user(user: User):
return user
Suppose someone sends:
{
"name": "Akhilesh",
"age": 27
}
FastAPI + Pydantic essentially do:
HTTP request
↓
JSON
↓
Pydantic validation
↓
User object
↓
your function
Your function receives a structured object.
You don't have to manually check:
if "name" not in data:
if "age" not in data:
if not isinstance(data["age"], int):
...
12. Pydantic also gives you serialization
Suppose:
class User(BaseModel):
name: str
age: int
user = User(name="Akhilesh", age=27)
You can do:
user.model_dump()
Result:
{
"name": "Akhilesh",
"age": 27
}
And:
user.model_dump_json()
gives JSON.
This is extremely useful when communicating between:
API
↓
Python application
↓
Database
↓
Another API
13. Pydantic also generates schemas
This is another major difference.
Given:
class User(BaseModel):
name: str
age: int
Pydantic can generate a JSON Schema describing the model.
Conceptually:
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
}
}
}
This is extremely useful for APIs.
FastAPI uses these schemas to generate OpenAPI documentation.
So you write:
class User(BaseModel):
name: str
age: int
and FastAPI can use it to understand:
What data does this API accept?
What types?
Which fields?
What does the response look like?
14. This is even more important in LLM
applications
Since you're working with RAG/LLM applications, this distinction is especially useful.
Imagine an LLM should return:
{
"answer": "SDTM is a standard...",
"confidence": 0.92,
"sources": [
"SDTMIG.pdf",
"Study_Specification.pdf"
]
}
You can define:
from pydantic import BaseModel
class RAGResponse(BaseModel):
answer: str
confidence: float
sources: list[str]
Now your application has a strict structure for the LLM output.
You can validate that the output has:
answer
→ string
confidence
→ float
sources
→ list of strings
This is one reason Pydantic appears so often in modern AI frameworks.
15. Another example: RAG request
Suppose your RAG API accepts:
{
"query": "What is SDTM?",
"top_k": 5
}
Define:
from pydantic import BaseModel, Field
class RAGRequest(BaseModel):
query: str = Field(min_length=1)
top_k: int = Field(default=5, gt=0, le=20)
Now your API automatically has rules:
query
└── must be a string
└── cannot be empty
top_k
└── must be an integer
└── default = 5
└── > 0
└── <= 20
Doing all of this manually with dataclasses would require considerably more code.
16. So should you always use Pydantic?
No.
This is important.
Use a dataclass when you primarily want:
"I need a convenient Python object to hold data."
Example:
@dataclass
class Embedding:
vector: list[float]
dimension: int
If the data is generated internally and you trust it, a dataclass may be perfectly appropriate.
Use Pydantic when you primarily need:
"I need to receive, validate, parse, serialize, or describe structured data."
Examples:
HTTP requests
HTTP responses
Configuration
Environment variables
LLM structured outputs
RAG inputs/outputs
External API responses
User input
JSON data
17. Simple analogy
Think of a dataclass as a labeled box:
┌─────────────────────────┐
│
User Box
│
│
│
│ name = Akhilesh
│
│ age
│
= 27
└─────────────────────────┘
Its main job is:
"Give me a convenient structure for storing these values."
Pydantic is more like a security checkpoint:
Incoming data
↓
┌──────────────────┐
│
Pydantic
│
│
CHECKPOINT
│
│
│
│ Is name valid?
│
│ Is age valid?
│
│ Is email valid?
│
│ Are constraints
│
│ satisfied?
│
└────────┬─────────┘
↓
Valid object
It doesn't just store the data.
It checks and processes the data before your application uses it.
18. Side-by-side comparison
Create structured objects
Type annotations
Automatic runtime
validation
Data parsing
Field constraints
Pydantic
dataclass
Feature
BaseModel
✅
✅
✅
✅
❌
✅
Limited
Manual
Manual/extra
JSON serialization
work
JSON Schema
Not built-in
Excellent for API input
Possible
Excellent for internal objects Excellent
FastAPI integration
Possible
LLM structured output
Possible
✅
✅
✅
✅
Excellent
✅
Excellent
Excellent
19. The key idea to remember
Don't think:
"Pydantic is a better dataclass."
Think:
dataclass
↓
"I want a structured Python object."
Pydantic
↓
"I want structured + validated + parsed data."
And this distinction explains why you see:
class User(BaseModel):
so often in FastAPI.
The data is coming from outside your Python program, so you want a validation boundary.
For your AI/RAG work, a very useful rule is:
External data
│
┌──────────┴──────────┐
│
│
API JSON
LLM output
│
│
└──────────┬──────────┘
↓
Pydantic
BaseModel
↓
Valid structured
data
↓
Your RAG/business
logic
That's the real reason Pydantic is used even though Python already has dataclass.
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )