Extensible Python: Robustness through Addition - PyCon 2024

pviafore 153 views 81 slides May 20, 2024
Slide 1
Slide 1 of 81
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76
Slide 77
77
Slide 78
78
Slide 79
79
Slide 80
80
Slide 81
81

About This Presentation

A talk given at PyCon 2024 about how you can write sustainable Python by understanding dependencies, composability, open-closed principles, and extensibility. Also covers topics such as Event-Driven Programming and Plug-in based Architecture


Slide Content

Software is both archeology and time-travel

Your job is to deliver value in a timely manner

How do you enable the future to be just as efficient ?

Extensibility

) Think about the humans

Extensibility Themes

1) Reduce Commit Complexity

2 ) Be Mindful of OCP

Open-Closed Principle

Code should be open to extension and closed for modification

When To Split Are easy things hard to do? Do you encounter pushback against similar features? Do you have consistently high estimates? Do commits contain large changesets? Are you mixing policy changes with mechanisms changes

Policies vs. Mechanisms

3 ) Separate Policies and Mechanisms

From backoff documentation

F rom Flask documentation

4 ) Data-Driven Designs

Data options Can you put your data in a collection? Configuration Files? Persistence Layers (i.e. databases, bucket storage, etc.)?

5 ) Develop Libraries First

Good Library Design README-driven Tested Easily Consumable Clearly-stated opinions Releasable (w/ Documentation) Extensible Composable

Composability

From Flask documentation

6 ) Create Building Blocks

Architectural Extensibility

Dependencies

Physical Dependencies

from typing import Callable @dataclass class Metadata: does_match: Callable [[ User ], bool ] if __name__ == "__main__" : run_self_test()

pip install requests

Physical Dependencies Hard-coded into source code Easy to follow from A to B Easily understandable Easy to find, hard to change Hard to substitute or mock out Understandable by static analysis tools

pipdeptree

pydeps

pyan3

What Happens If You Need Something To Change?

A B C

A B C

A B D

A B Whatever

Logical Dependencies

requests.post( "table-management/pizza-made" , { "id" : order, "pizza" : pizza.to_json() }) # meal is an abstract type meal: Meal = meal_factory.create( "pizza" )

Logical Dependencies Typically determined at run-time Readability suffers Debuggability suffers Hard to find, easy to change Easy to substitute or mock out Not understandable by static analysis tools Crucial for Abstraction

7 ) Trade-off Dependencies Judiciously

Event-Driven Architectures

def complete_order(order: Order ): package_order(order) notify_customer_that_order_is_done(order) notify_restaurant_that_order_is_done(order)

Architectural Extensibility

Producer Transport Consumer

Producer Transport Consumer

pypubsub

from pubsub import pub def notify_customer_that_meal_is_done(order: Order ): # ... snip ... pub.subscribe(notify_customer_that_meal_is_done, "meal-done" )

from pubsub import pub def complete_order(order: Order ): package_order(order) pub.publish( "meal-done" , order)

8 ) Use Event-Driven Architectures to Decouple Producers and Consumers

Pluggable Architectures

Plug-in examples pytest poetry hypothesis pylint

Plugging Into Algorithms

@dataclass class PizzaCreationFunctions: prepare_ingredients: Callable add_pre_bake_toppings: Callable add_post_bake_toppings: Callable

def create_pizza( pizza_creation_functions: PizzaCreationFunctions ): pizza_creation_functions.prepare_ingredients() roll_out_pizza_base() pizza_creation_functions.add_pre_bake_toppings() bake_pizza() pizza_creation_functions.add_post_bake_toppings()

pizza_creation_functions = PizzaCreationFunctions( prepare_ingredients=mix_zaatar, add_pre_bake_toppings=add_meat_and_halloumi, add_post_bake_toppings=drizzle_olive_oil ) create_pizza(pizza_creation_functions)

pizza_creation_functions = PizzaCreationFunctions( prepare_ingredients=cook_bulgogi, add_pre_bake_toppings=add_bulgogi_toppings, add_post_bake_toppings=garnish_with_scallions_and_sesame ) create_pizza(pizza_creation_functions)

Plugging Into Packages

Stevedore

from abc import abstractmethod from typing import runtime_checkable, Protocol @runtime_checkable class UltimateKitchenAssistantModule( Protocol ): ingredients: list [ Ingredient ] @abstractmethod def get_recipes() -> list [ Recipe ]: raise NotImplementedError @abstractmethod def prepare_dish(inventory: dict [ Ingredient , Amount ], recipe: Recipe ) -> Dish : raise NotImplementedError

class PastaModule( UltimateKitchenAssistantModule ): def __init__(self): self.ingredients = [ "Linguine" , # ... snip ... ] def get_recipes(self) -> list [ Recipe ]: # ... snip returning all possible recipes ... def prepare_dish(self, inventory: dict [ Ingredient , Amount ], recipe: Recipe ) -> Dish : # interact with Ultimate Kitchen Assistant

from stevedore import extension def get_all_recipes() -> list [ Recipe ]: mgr = extension.ExtensionManager( namespace= 'ultimate_kitchen_assistant.recipe_maker' , invoke_on_load= True , ) def get_recipes(extension): return extension.obj.get_recipes() return list(itertools.chain(mgr.map(get_recipes)))

from setuptools import setup setup( name= 'ultimate_kitchen_assistant' , version= '1.0' , # . ... snip .... entry_points={ 'ultimate_kitchen_assistant.recipe_maker' : [ 'pasta_maker = ultimate_kitchen_assistant.pasta_maker:PastaModule' ], }, )

9 ) Use Pluggable Python for Extension Points

0) Think about the humans 1) Reduce Commit Complexity 2) Be Mindful of OCP 3) Separate Policies and Mechanisms 4) Consider Data-Driven Design 5) Develop Libraries First 6) Create Building Blocks 7) Trade-off Dependencies Judiciously 8) Use Event-Driven Architectures to Decouple Producers/Consumers 9) Use Pluggable Python for Extension Points

10 ) Don't Overextend Yourself

Who Am I? Principal Software Engineer Cloud Software Group Owner of Kudzera, LLC Author of Robust Python Organizer of HSV.py

Extensible Python: Robustness Through Addition Patrick Viafore