# DTOs in Python: A Pragmatic Approach Yes, Python developers absolutely use DTOs, but they often look different from Java or C# implementations. The concept remains the same - transferring data between layers - but Python's dynamic nature allows for more flexible approaches. ## Common Python DTO Patterns ### 1. Dictionaries The simplest form of DTOs in Python - plain dictionaries: ```/dev/null/dict_dto.py#L1-14 def get_user_profile(user_id): # Fetch user from database user = User.query.get(user_id) # Create a dictionary that serves as a DTO user_dto = { 'id': user.id, 'username': user.username, 'email': user.email # Note: password_hash is intentionally excluded } return user_dto ``` ### 2. Dataclasses (Python 3.7+) A more structured approach using Python's dataclasses: ```/dev/null/dataclass_dto.py#L1-17 from dataclasses import dataclass, asdict @dataclass class UserDTO: id: int username: str email: str def get_user_profile(user_id): user = User.query.get(user_id) # Create a DTO from the entity user_dto = UserDTO( id=user.id, username=user.username, email=user.email ) return user_dto # or asdict(user_dto) for dictionary conversion ``` ### 3. Pydantic Models (Popular in FastAPI) Pydantic provides data validation and is widely used for DTOs in FastAPI: ```/dev/null/pydantic_dto.py#L1-23 from pydantic import BaseModel from typing import List class OrderItemDTO(BaseModel): id: int product_name: str quantity: int price: float class OrderDetailDTO(BaseModel): id: int customer_name: str total: float items: List[OrderItemDTO] @app.get("/orders/{order_id}") def get_order(order_id: int): order = db.get_order(order_id) # Pydantic automatically handles the conversion return OrderDetailDTO( id=order.id, customer_name=order.customer.name, total=order.total, items=[OrderItemDTO(**item.__dict__) for item in order.items] ) ``` ### 4. Named Tuples Lightweight immutable DTOs: ```/dev/null/namedtuple_dto.py#L1-12 from collections import namedtuple # Define DTO structure UserDTO = namedtuple('UserDTO', ['id', 'username', 'email']) def get_user_profile(user_id): user = User.query.get(user_id) # Create a named tuple as DTO user_dto = UserDTO(id=user.id, username=user.username, email=user.email) return user_dto ``` ## Framework-Specific Approaches ### Django REST Framework Serializers Django's approach to DTOs is through serializers: ```/dev/null/django_serializers.py#L1-19 from rest_framework import serializers class OrderItemSerializer(serializers.Serializer): id = serializers.IntegerField() product_name = serializers.CharField() quantity = serializers.IntegerField() price = serializers.DecimalField(max_digits=10, decimal_places=2) class OrderSerializer(serializers.Serializer): id = serializers.IntegerField() customer_name = serializers.CharField() total = serializers.DecimalField(max_digits=10, decimal_places=2) items = OrderItemSerializer(many=True) # Usage in view @api_view(['GET']) def get_order(request, order_id): order = Order.objects.get(id=order_id) serializer = OrderSerializer(order) return Response(serializer.data) ``` ### SQLAlchemy ORM with Marshmallow Combining SQLAlchemy with Marshmallow for serialization: ```/dev/null/marshmallow_example.py#L1-28 from marshmallow import Schema, fields # Database entity class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String) email = db.Column(db.String) password_hash = db.Column(db.String) created_at = db.Column(db.DateTime) # DTO schema class UserSchema(Schema): id = fields.Int() username = fields.Str() email = fields.Str() # Note: password_hash is excluded user_schema = UserSchema() @app.route('/users/') def get_user(user_id): user = User.query.get_or_404(user_id) # Convert to DTO user_dto = user_schema.dump(user) return jsonify(user_dto) ``` ## When to Use DTOs in Python Even though Python is more flexible, you should still consider using DTOs when: 1. **API Boundaries**: Exposing data through REST APIs 2. **Security Concerns**: Need to filter sensitive data 3. **Serialization Requirements**: Converting between formats (JSON, XML, etc.) 4. **Domain Separation**: Keeping domain models separate from presentation 5. **Documentation**: Making API contracts explicit (especially with Pydantic and OpenAPI) ## Comparing Python and Java DTO Approaches ```/dev/null/comparison.txt#L1-12 ┌───────────────────┬────────────────────────┬────────────────────────┐ │ Aspect │ Java │ Python │ ├───────────────────┼────────────────────────┼────────────────────────┤ │ Formality │ Highly structured │ More pragmatic │ │ Type Safety │ Compile-time checking │ Runtime verification │ │ Implementation │ Explicit classes │ Various options │ │ Mapping │ MapStruct, ModelMapper │ Direct assignment, │ │ │ │ marshmallow, Pydantic │ │ Verbosity │ More boilerplate │ Less boilerplate │ │ Integration │ Separate from │ Often integrated with │ │ │ serialization │ serialization │ └───────────────────┴────────────────────────┴────────────────────────┘ ``` ## Practical Python DTO Example for a RESTful API Here's a complete example using Flask, SQLAlchemy, and dataclasses: ```/dev/null/flask_example.py#L1-56 from flask import Flask, jsonify from flask_sqlalchemy import SQLAlchemy from dataclasses import dataclass from typing import List app = Flask(__name__) db = SQLAlchemy(app) # Database models class Order(db.Model): id = db.Column(db.Integer, primary_key=True) customer_name = db.Column(db.String) customer_email = db.Column(db.String) total = db.Column(db.Float) class OrderItem(db.Model): id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('order.id')) product_name = db.Column(db.String) product_sku = db.Column(db.String) quantity = db.Column(db.Integer) price = db.Column(db.Float) order = db.relationship('Order', backref='items') # DTOs using dataclasses @dataclass class OrderItemListDTO: id: int product_name: str quantity: int price: float @dataclass class OrderListDTO: id: int customer_name: str total: float @dataclass class OrderDetailDTO: id: int customer_name: str total: float items: List[OrderItemListDTO] # API endpoints @app.route('/api/orders') def get_orders(): orders = Order.query.all() # Convert to ListOfMasters DTOs orders_dto = [OrderListDTO(id=o.id, customer_name=o.customer_name, total=o.total) for o in orders] return jsonify(orders_dto) @app.route('/api/orders/') def get_order(order_id): order = Order.query.get_or_404(order_id) # Convert to DetailOfMaster DTO with embedded ListOfDetails items_dto = [OrderItemListDTO(id=i.id, product_name=i.product_name, quantity=i.quantity, price=i.price) for i in order.items] order_dto = OrderDetailDTO( id=order.id, customer_name=order.customer_name, total=order.total, items=items_dto ) return jsonify(order_dto) ``` ## Conclusion Python definitely uses the DTO pattern, but with a more pragmatic and flexible approach than strictly typed languages. The key principles remain the same: 1. **Separate concerns** between database models and API responses 2. **Control data exposure** to prevent leaking sensitive information 3. **Optimize data transfer** by including only what's needed 4. **Make API contracts clear** for consumers Whether you call them DTOs, serializers, schemas, or just data containers, the pattern serves the same essential purpose across all languages - creating a clear boundary between your internal data models and what you expose to the outside world.