Skip to content

Commit e6e8b07

Browse files
committed
feat(employee): implement employee model with Pydantic validation
1 parent 043a6e4 commit e6e8b07

4 files changed

Lines changed: 498 additions & 0 deletions

File tree

python_fundamentals/exercises/employee_salary_module/__init__.py

Whitespace-only changes.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
2+
# Assignment 01 — Employee Pydantic Model
3+
4+
## Objective
5+
6+
Build a self-validating `Employee` model using **Pydantic**.
7+
8+
This assignment combines:
9+
10+
* Object-Oriented Programming
11+
* Type Hints
12+
* Pydantic
13+
* `Field`
14+
* `EmailStr`
15+
* `field_validator`
16+
* Instance Methods
17+
* Pytest
18+
19+
---
20+
21+
# Learning Goals
22+
23+
After completing this assignment, you should be able to:
24+
25+
* Create a Pydantic model.
26+
* Validate data using `Field`.
27+
* Write custom validation using `field_validator`.
28+
* Normalize user input.
29+
* Add instance methods to a Pydantic model.
30+
* Test validation using `pytest`.
31+
32+
---
33+
34+
# Requirements
35+
36+
Create a file:
37+
38+
```text
39+
employee.py
40+
```
41+
42+
Create the following model:
43+
44+
```python
45+
class Employee(BaseModel):
46+
```
47+
48+
---
49+
50+
# Fields
51+
52+
Implement the following fields:
53+
54+
| Field | Type |
55+
| ----------- | -------- |
56+
| employee_id | str |
57+
| name | str |
58+
| email | EmailStr |
59+
| salary | float |
60+
61+
---
62+
63+
# Validation Rules
64+
65+
## employee_id
66+
67+
Rules:
68+
69+
* Must start with `"EMP-"`.
70+
71+
Example:
72+
73+
```text
74+
EMP-1001
75+
```
76+
77+
Use:
78+
79+
* `field_validator`
80+
81+
---
82+
83+
## name
84+
85+
Rules:
86+
87+
* Remove leading and trailing whitespace.
88+
* Convert to Title Case.
89+
* Minimum length: 3 characters.
90+
* Alphabetic characters only.
91+
92+
Examples:
93+
94+
Valid
95+
96+
```text
97+
" tejas dixit "
98+
```
99+
100+
101+
102+
```text
103+
Tejas Dixit
104+
```
105+
106+
Invalid
107+
108+
```text
109+
Tejas123
110+
```
111+
112+
---
113+
114+
## email
115+
116+
Use:
117+
118+
```python
119+
EmailStr
120+
```
121+
122+
---
123+
124+
## salary
125+
126+
Rules:
127+
128+
* Greater than zero.
129+
130+
Use:
131+
132+
```python
133+
Field(gt=0)
134+
```
135+
136+
---
137+
138+
# Instance Methods
139+
140+
Implement the following methods.
141+
142+
---
143+
144+
## annual_salary()
145+
146+
Returns:
147+
148+
```text
149+
monthly salary × 12
150+
```
151+
152+
Example:
153+
154+
```python
155+
employee.annual_salary()
156+
```
157+
158+
---
159+
160+
## apply_raise()
161+
162+
Accepts:
163+
164+
```python
165+
percentage: float
166+
```
167+
168+
Updates the employee salary.
169+
170+
Example:
171+
172+
Before
173+
174+
```text
175+
25000
176+
```
177+
178+
After
179+
180+
```python
181+
employee.apply_raise(10)
182+
```
183+
184+
185+
186+
```text
187+
27500
188+
```
189+
190+
This is a **state-changing method**.
191+
192+
---
193+
194+
# Demonstration
195+
196+
Create:
197+
198+
* One valid employee.
199+
* One invalid email example.
200+
* One invalid name example.
201+
* One invalid salary example.
202+
203+
Catch:
204+
205+
```python
206+
ValidationError
207+
```
208+
209+
Print the validation errors.
210+
211+
---
212+
213+
# Testing
214+
215+
Create:
216+
217+
```text
218+
test_employee.py
219+
```
220+
221+
Write at least four tests.
222+
223+
Required:
224+
225+
* Valid employee
226+
* Invalid email
227+
* Annual salary
228+
* Apply raise
229+
230+
---
231+
232+
# Quality Gates
233+
234+
Your solution must pass:
235+
236+
```bash
237+
uv run ruff check .
238+
uv run ruff format --check .
239+
uv run mypy .
240+
uv run pytest -v
241+
```
242+
243+
---
244+
245+
# Constraints
246+
247+
* Use type hints everywhere.
248+
* Write docstrings for all public methods.
249+
* Do not use `input()`.
250+
* Do not use global variables.
251+
* Keep validation inside the model.
252+
253+
---
254+
255+
# Expected Learning Outcome
256+
257+
By completing this assignment, you should understand that a **Pydantic model is still a Python class**.
258+
259+
It can contain:
260+
261+
* Fields
262+
* Validators
263+
* Instance methods
264+
* Business logic
265+
266+
Validation protects the model's data, while methods implement the model's behavior.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import re
2+
3+
from pydantic import BaseModel, EmailStr, Field, ValidationError, field_validator
4+
5+
ALLOWED_DEPARTMENTS: frozenset[str] = frozenset(
6+
{"HR", "MARKETING", "ADMIN", "OPERATIONS"}
7+
)
8+
9+
10+
class Employee(BaseModel):
11+
employee_id: str = Field(description="Employee ID")
12+
name: str = Field(
13+
description="Employee name",
14+
min_length=3,
15+
max_length=20,
16+
)
17+
email: EmailStr = Field(description="Employee email")
18+
department: str = Field(description="Employee department")
19+
salary: float = Field(description="Monthly salary", gt=0)
20+
age: int = Field(description="Employee age", ge=18, le=60)
21+
is_active: bool = Field(
22+
default=False,
23+
description="Employee active status",
24+
)
25+
26+
@field_validator("name")
27+
@classmethod
28+
def validate_name(cls, name: str) -> str:
29+
"""Validate Employee name and return transomed name"""
30+
name = " ".join(name.strip().split()).title()
31+
32+
if not name.replace(" ", "").isalpha():
33+
raise ValueError("Name must contain only alphabetic characters.")
34+
35+
return name
36+
37+
@field_validator("department")
38+
@classmethod
39+
def validate_department(cls, department: str) -> str:
40+
"""Validate Department"""
41+
department = department.strip().upper()
42+
43+
if department not in ALLOWED_DEPARTMENTS:
44+
raise ValueError(f"Department must be one of {sorted(ALLOWED_DEPARTMENTS)}")
45+
46+
return department
47+
48+
@field_validator("employee_id")
49+
@classmethod
50+
def validate_employee_id(cls, employee_id: str) -> str:
51+
"""Validate Employee id"""
52+
employee_id = employee_id.strip().upper()
53+
54+
if not re.fullmatch(r"EMP-\d+", employee_id):
55+
raise ValueError("Employee ID must be in the format EMP-1001.")
56+
57+
return employee_id
58+
59+
def annual_salary(self) -> float:
60+
"""Return annual salary."""
61+
return self.salary * 12
62+
63+
def apply_raise(self, percentage: float) -> None:
64+
"""Apply a percentage raise and return the updated salary."""
65+
66+
if percentage <= 0:
67+
raise ValueError("Percentage must be greater than 0.")
68+
69+
self.salary += self.salary * (percentage / 100)
70+
return None
71+
72+
73+
def main() -> None:
74+
try:
75+
employee = Employee(
76+
employee_id="EMP-1001",
77+
name="Tejas Dixit",
78+
email="tejasdixit17@zohomail.in",
79+
department="hr",
80+
salary=25000,
81+
age=25,
82+
is_active=True,
83+
)
84+
85+
print(employee.model_dump())
86+
print(f"Annual Salary: {employee.annual_salary():,.2f}")
87+
employee.apply_raise(percentage=10)
88+
print(f"Updated Salary:{employee.salary}")
89+
90+
except ValidationError as exc:
91+
print(exc)
92+
93+
94+
if __name__ == "__main__":
95+
main()

0 commit comments

Comments
 (0)