Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django dalle trincee: pattern e pratiche dopo 1...

Django dalle trincee: pattern e pratiche dopo 15 anni di esperienza su Django

In 15 anni di esperienza, ne ho viste, di cose, che voi umani ...
Nonostante Django fornisca un'architettura di base molto pulita e linee guida per aiutare una buona organizzazione del progetto, le cose possono sfuggire di mano e portare rapidamente il progetto ad essere un piatto di spaghetti in salsa pythonica.

La ricetta perfetta non esiste, perché ogni progetto di una certa complessità ha le sue particolarità e le sue caratteristiche che richiedono un approccio individuale.

Un buon progetto parte però dal processo di sviluppo, si parte quindi con alcune riflessioni sulle pratiche che nel tempo sono state efficaci negli migliorare la qualità dello sviluppo.

Successivamente, saranno presentati alcuni pattern, dettati da un approccio pragmatico e basato sull'esperienza, che possano essere un'ispirazione per i propri progetti.

I principi che ispirano questi pattern sono di favorire l'estendibilità del codice e la manutenibilità, e per migliorare la comunicazione e la documentazione del progetto per gestire in modo più efficiente il ciclo di vita del progetto stesso

Avatar for Iacopo Spalletti

Iacopo Spalletti

May 30, 2025
Tweet

More Decks by Iacopo Spalletti

Other Decks in Programming

Transcript

  1. Django dalle Trincee Consigli e pattern da 15 anni di

    django Iacopo Spalletti @ Nephila PyConIT 2025 30 May 2025 Iacopo Spalletti - @yakkys
  2. Issues and Symptoms messy code fragile architecture challenging maintenance high

    costs of adding features complex deploys Iacopo Spalletti - @yakkys
  3. Why? Every person is different Unique combination of strengths and

    weaknesses No silver bullet Iacopo Spalletti - @yakkys
  4. Why? Every team is different Unique combination of people and

    conditions No silver bullet Iacopo Spalletti - @yakkys
  5. Why? Every project is different Unique combination of constraints and

    challenges No silver bullet Iacopo Spalletti - @yakkys
  6. What if I work alone? In team with your future

    self Iacopo Spalletti - @yakkys
  7. In a team communication introducing new approaches improve team skills

    grow junion members Code Review Iacopo Spalletti - @yakkys
  8. Sphinx Easy to integrate in the same repository Enable autodoc

    features to extract information from code Static rendering Documentation 1 # 2 import os 3 import sys 4 sys.path.insert(0, os.path.absp 5 project = 'My Project' 6 copyright = 'Myself' 7 author = 'myself' 8 extensions = ['sphinx.ext.autod 9 templates_path = ['_templates'] 10 exclude_patterns = ['_build'] 11 #html_static_path = ['_static'] 1 Architecture 2 ============== 3 .... 4 5 Models 6 =========== 7 8 .. automodule:: my_project.my_a 9 :members: Iacopo Spalletti - @yakkys
  9. Being in Control Client doesn’t pay for tests The boss

    thinks they’re a waste Do I really need tests? It’s obvious that this code works Tests Iacopo Spalletti - @yakkys
  10. You don’t have to be perfect You don’t need TDD

    You don’t need 100% coverage Something is better than nothing Tests Iacopo Spalletti - @yakkys
  11. What to automate? Whatever can change in a commit Code

    style Tests Security checks dependencies: pip-audit static code analysis: bandit Builds Deployments Iacopo Spalletti - @yakkys
  12. CI Plenty of hosted options (Github Actions, …) Self-hosted platforms

    available (Gitlab, …) Iacopo Spalletti - @yakkys
  13. Beyond CI Yeah, CI is cool, but have you tried

    automating commands? bash Just Fabric Ansible Terraform / OpenTofu Iacopo Spalletti - @yakkys
  14. Deployment Example 1 @task 2 def deploy(connection): 3 if not

    connection.run(f"test -d {repo_dir}", warn=True).ok: 4 commands.append(f"git clone {repo_url} {repo_root}") 5 else: 6 commands.append(f"cd {repo_root} && git fetch") 7 connection.run(f"cd {repo_root} && git checkout origin/{branch}") 8 connection.run(f"cd {repo_root} && python{python} -m venv {VIRTUALENV}") 9 connection.run(f"cd {repo_root} && {VIRTUALENV}/bin/python -m pip install -r requirements_live.txt") 10 connection.run(f"cd {repo_root} &&" f"{VIRTUALENV}/bin/python manage.py migrate --noinput") 11 connection.run(f"cd {repo_root} &&" f"{VIRTUALENV}/bin/python manage.py collectstatic -c --noinput") Iacopo Spalletti - @yakkys
  15. Iterate, Iterate, Iterate gain confidence gain velocity to invest in

    new practices gain reliability Iacopo Spalletti - @yakkys
  16. Embrace idiomatic Django Generic views are your friends Fat Model,

    Skinny View Fat Model, Chubby Querysets Be pragmatic Iacopo Spalletti - @yakkys
  17. Generic views are your friends Embrace idiomatic Django 1 class

    TransferFundView(UpdateView): 2 model = BankAccount 3 fields = ["amount"] 4 5 class BankAccount(models.Model): 6 amount = models.DecimalField(decimal_places=2, max_digits=10) Iacopo Spalletti - @yakkys
  18. Generic views are your friends Embrace idiomatic Django 1 class

    TransferFundView(UpdateView): 2 model = BankAccount 3 form_class = TransferFundForm 4 5 class TransferFundForm: 6 class Meta: 7 model = BankAccount 8 fields = ["amount"] 9 10 def clean_amount(self): 11 if self.cleaned_data["amount"] < 0: 12 raise ValidationError("Amount must be positive") 13 return self.cleaned_data["amount"] 14 15 class BankAccount(models.Model): 16 amount = models.DecimalField(decimal_places=2, max_digits=10) Iacopo Spalletti - @yakkys
  19. Fat Model, Skinny View Embrace idiomatic Django 6 def save(self,

    *args, **kwargs): 7 return self.instance.transfer_amount(self.cleaned_data["amount"]) 12 def transfer_amount(self, new_amount): 13 self.amount = new_amount 14 self.save() 15 return self 1 class TransferFundForm: 2 class Meta: 3 model = BankAccount 4 fields = ["amount"] 5 8 9 class BankAccount(models.Model): 10 amount = models.DecimalField(decimal_places=2, max_digits=10) 11 Iacopo Spalletti - @yakkys
  20. Fat Model, Skinny View Permissions Object filtering Template rendering Form

    processing Embrace idiomatic Django 1 class TransferFundView(UpdateView): 2 model = BankAccount 3 form_class = TransferFundForm 4 5 ??? Iacopo Spalletti - @yakkys
  21. Fat Model, Chubby QuerySet Embrace idiomatic Django 1 class BankAccountQuerySet(models.QuerySet):

    2 3 def green_accounts(self): 4 return self.filter(amount__gt=0) 5 6 class BankAccount(models.Model): 7 amount = models.DecimalField(decimal_places=2, max_digits=10) 8 9 objects = BankAccountQuerySet.as_manager() 10 11 rich_accounts = BankAccount.objects.green_account() Iacopo Spalletti - @yakkys
  22. Example Service 1 class HandleTransferFunds: 2 @staticmethod 3 def deposit(source_account,

    target_account, amount): 4 with atomic(): 5 if source_account.has_available(amount): 6 source_account.remove(amount) 7 target_account.add(amount) 8 9 HandleTransferFunds.deposit(account_1, account_2, 100) Iacopo Spalletti - @yakkys
  23. Composition >> Inheritance django is all about inheritance but your

    logic doesn’t have to Service Iacopo Spalletti - @yakkys
  24. Stateless? Service 1 @dataclass 2 class HandleTransferFunds: 3 source: Account

    4 target: Account 5 amount: Decimal 6 7 def run(self): 8 with atomic(): 9 if self.source.has_available(self.amount): 10 self.source.remove(self.amount) 11 self.target.add(self.amount) 12 13 HandleTransferFunds(account_1, account_2, 100).run() Iacopo Spalletti - @yakkys
  25. Oops Service 1 def handle_transfer_funds(source_account, target_account, amount): 2 with atomic():

    3 if source_account.has_available(amount): 4 source_account.remove(amount) 5 target_account.add(amount) Iacopo Spalletti - @yakkys
  26. A more realistc example Service 1 @dataclass 2 class TransferVideo:

    3 source_account: str 4 target_account: str 5 video_source_class: AbstractVideoService 6 video_target_class: AbstractVideoService 7 8 def _get_video_client(self, video_class, account): 9 instance = video_class(account) 10 return instance 11 12 def _transfer(self, source, target): 13 source_video = source_client.get_video() 14 target_client.upload_video(source_video) 15 16 def run(self): 17 source_client = self._get_video_client(self.video_source_class, self.source_account) 18 target_client = self._get_video_client(self.video_target_class, self.target_account) 19 self._transfer(source_client, target_client) Iacopo Spalletti - @yakkys
  27. Django sequence Logic Model Form View Logic Model Form View

    Instantiate Instantiate Validate Errors Save Execute Execute Execute Errors Iacopo Spalletti - @yakkys
  28. Service sequence Model Service Form View Model Service Form View

    Instantiate Instantiate Validate Errors Instantiate Execute Save Errors Errors Iacopo Spalletti - @yakkys
  29. Django sequence Adding logic 13 def handle_transfer_funds(source_account, target_account, amount): 14

    with atomic(): 15 if source_account.has_available(amount): 16 source_account.remove(amount) 17 target_account.add(amount) 1 class TransferFundView(UpdateView): 2 model = BankAccount 3 form_class = TransferFundForm 4 5 class TransferFundForm: 6 class Meta: 7 model = BankAccount 8 fields = ["amount"] 9 10 class BankAccount(models.Model): 11 amount = models.DecimalField(decimal_places=2, max_digits=10) 12 Iacopo Spalletti - @yakkys
  30. View hard to reuse hard to test logic scattered across

    the codebase 🤨 Adding logic 1 class TransferFundView(UpdateVi 2 model = BankAccount 3 form_class = TransferFundFo 4 5 def form_valid(self, form): 6 ... 7 with atomic(): 8 if source_account.h 9 source_account. 10 target_account. 11 return super().form_val Iacopo Spalletti - @yakkys
  31. Form less hard to reuse less hard to test logic

    scattered across the codebase 😐 Adding logic 1 class TransferFundForm(models.M 2 3 class Meta: 4 model = BankAccount 5 fields = ["amount"] 6 7 def __init__(self, target, 8 self.target = target 9 ... 10 11 def save(self, *args, **kwa 12 with atomic(): 13 if instance.has_ava 14 instance.remove 15 self.target.add 16 return instance Iacopo Spalletti - @yakkys
  32. Service Adding logic 1 @dataclass 2 class HandleTransferFunds: 3 source:

    Account 4 target: Account 5 amount: Decimal 6 7 def run(self): 8 with atomic(): 9 if self.source.has_available(self.amount): 10 self.source.remove(self.amount) 11 self.target.add(self.amount) Iacopo Spalletti - @yakkys
  33. Service Adding logic 1 @dataclass 2 class HandleTransferFunds: 3 source:

    Account 4 target: Account 5 amount: Decimal 6 7 def validate(): 8 if not self.source.has_available(self.amount): 9 raise ValidationError("Not enough funcs") 10 return True 11 12 def run(self): 13 with atomic(): 14 if self.validate(): 15 self.source.remove(self.amount) 16 self.target.add(self.amount) Iacopo Spalletti - @yakkys
  34. Form Adding logic 11 def save(self, commit=True): 12 try: 13

    instance = self.logic.run() 14 except ValidationError as e: 15 self.add_error(None, e) 16 raise 1 class TransferFundForm(models.ModelForm): 2 3 class Meta: 4 model = BankAccount 5 fields = ["amount"] 6 7 def clean(): 8 self.logic.validate() 9 ... 10 17 return instance Iacopo Spalletti - @yakkys
  35. View Adding logic 5 def form_valid(self, form): 6 try: 7

    return super().form_valid(form) 8 except (ValueError, ValidationError) as e: 9 return super().form_invalid(form) 1 class TransferFundView(UpdateView) 2 model = BankAccount 3 form = TransferFundForm 4 Iacopo Spalletti - @yakkys
  36. Tradeoffs Reuse 👍 Test 👍 Isolation 👍 Complexity 👎 Non

    idiomatic code 👎 Adding logic Iacopo Spalletti - @yakkys
  37. Case Listen on a webhook and run some logic on

    a mixed set of web applications Pub-sub Iacopo Spalletti - @yakkys
  38. Publisher Pub-sub 17 request.app.state.redis_client.publish("order", json.dumps(message)) 1 def get_publisher(app): 2 app.state.redis_client

    = redis.Redis.from_url(url=url) 3 yield 4 5 app = FastAPI(lifespan=get_publisher) 6 7 @app.api_router("/send_message/{msg_type}/{order}/{status}/") 8 async def send_message(request, msg_type: str, order: int, status: str): 9 message = { 10 "type": msg_type, 11 "message": { 12 "order": order, 13 "date": now().isoformat("%c"), 14 "status": status, 15 }, 16 } 18 return Response("ok") Iacopo Spalletti - @yakkys
  39. Subscriber - Channels version Pub-sub 12 while await channel.wait_message(): 13

    msg = await channel.get_json() 14 await MessageStatusDispatcher(self, msg).execute() 1 class OrderStatusConsumer(AsyncJsonWebsocketConsumer): 2 3 async def connect(self): 4 await self._subscribe() 5 6 async def _subscribe(): 7 redis = await aioredis.create_redis(settings.REDIS_URL) 8 subscriber_object = await redis.subscribe("order") 9 self._read_messages(subscriber_object[0]) 10 11 async def _read_messages(msg, channel): Iacopo Spalletti - @yakkys
  40. Subscriber - Celery version Pub-sub 10 for message in pubsub.listen():

    11 message_handler(message) 1 @worker_ready.connect 2 def at_start(sender, **kwargs): 3 start_redis_subscriber_task.delay() 4 5 @shared_task 6 def start_redis_subscriber_task(): 7 redis_client = redis.Redis.from_url(url=settings.REDIS_URL) 8 pubsub = redis_client.pubsub() 9 pubsub.subscribe("order") Iacopo Spalletti - @yakkys
  41. Handling messages Pub-sub 1 HANDLERS = { 2 "paid": PaidOrder,

    3 "shipped": ShippedOrder, 4 ... 5 } 6 7 def message_handler(message): 8 handler_class = HANDLERS[message["type"]] 9 handler_class(message).run() Iacopo Spalletti - @yakkys
  42. Strengths Full decoupling publisher / subscriber Mixed technologies Different use

    cases Webhooks IoT … Pub-sub Iacopo Spalletti - @yakkys
  43. Iacopo Spalletti Founder and CTO @Nephila_digital Djangonaut and open source

    developer @[email protected] https://github.com/yakky https://speakerdeck.com/yakky Iacopo Spalletti - @yakkys
  44. Linkography / 1 Team Tuckman’s stages of group development Automation

    tools bash Just Fabric Ansible Terraform OpenTofu Iacopo Spalletti - @yakkys
  45. Linkography / 2 Useful packages django-filter django-model-utils DDD Implementing Domain-Driven

    Design in Django Decoding DDD: A Three-Tiered Approach to Django Projects Against service layer Iacopo Spalletti - @yakkys