├── .gitignore ├── LICENSE ├── README.md ├── code ├── 02-static-vs-dynamic-languages │ ├── motorcycle.js │ ├── motorcycle.ts │ ├── motorcycles.cs │ ├── motorcycles.swift │ ├── package-lock.json │ ├── package.json │ ├── py-no-types.py │ └── py-types.py ├── 03-typing-in-python │ ├── calculator.py │ ├── calculator.pyi │ ├── p1_variables.py │ ├── p2_functions.py │ ├── p3_collections.py │ ├── p4_classes.py │ └── p5_external_types.py ├── 04-frameworks-built-on-typing │ ├── d1_parsing_with_pydantic.py │ ├── d2_parsing_weather.py │ ├── weather_models.py │ └── web_example │ │ ├── IMPORT_DATA.md │ │ ├── __init__.py │ │ ├── api │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── recent_package_model.py │ │ │ └── stats_model.py │ │ ├── package_api.py │ │ └── stats_api.py │ │ ├── infrastructure │ │ ├── __init__.py │ │ ├── mongo_setup.py │ │ └── time_utils.py │ │ ├── main.py │ │ ├── models │ │ ├── __init__.py │ │ ├── package.py │ │ ├── release_analytics.py │ │ └── user.py │ │ ├── services │ │ ├── __init__.py │ │ ├── package_service.py │ │ └── user_service.py │ │ ├── static │ │ └── css │ │ │ └── site.css │ │ └── templates │ │ └── index.html ├── 05-tooling │ ├── my_test.py │ ├── mypy.ini │ ├── runtime_example.py │ ├── runtime_speed.py │ ├── runtime_speed_checked.py │ └── timd.py ├── 06-orthogonal-typing │ └── protocol_app.py └── 07-typing-guidance-patterns │ ├── minimalism.py │ ├── motorbike.py │ ├── point_of_no_return.py │ └── refactoring_types.py ├── course_image └── type-hints.webp ├── requirements.piptools └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | /.idea/ 162 | /code/02-static-vs-dynamic-languages/node_modules/.package-lock.json 163 | node_modules/ 164 | /code/02-static-vs-dynamic-languages/mc 165 | /.ruff_cache/ 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Talk Python 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rock Solid Python with Type Hints Course 2 | [![](course_image/type-hints.webp)](https://training.talkpython.fm/courses/python-type-hint-course-with-hands-on-examples) 3 | 4 | ## Course summary 5 | 6 | When Python was originally invented way back in 1989, it was a truly dynamic and typeless programming language. But **that all changed in Python 3.5 when type "hints" were added to the language**. Over time, amazing frameworks took that idea and ran with it. They build powerful and type safe(er) frameworks. Some of these include Pydantic, FastAPI, Beanie, SQLModel, and many many more. **In this course, you'll learn the ins-and-outs of Python typing** in the language, explore some popular frameworks using types, and get some excellent advice and guidance for using types in your applications and libraries. 7 | 8 | ## What topics are covered 9 | 10 | In this course, you will: 11 | 12 | * **Compare popular static languages with Python** (such as Swift, C#, TypeScript, and others) 13 | * See a exact clone of a **dynamic Python codebase along side the typed version** 14 | * Learn how and when to **create typed variables** 15 | * Understand **Python's strict nullability** in its type system 16 | * Specify **constant** (unchangeable) variables and values 17 | * Reduce SQL injection attacks with LiteralString 18 | * Uses **typing with Python functions** and methods 19 | * Use **typing with classes** and class variables 20 | * Work with multiple numerical types with **Python's numerical type ladder** 21 | * Use **Pydantic to model and parse** complex data in a type strict manner 22 | * **Create an API with FastAPI** that exchanges data with type integrity 23 | * **Query databases with Pydantic** using the Beanie ODM 24 | * **Create CLI apps using type information** to define the CLI interface 25 | * Leverage **mypy for verifying the integrity** of your entire codebase in CI/CD 26 | * Add runtime type safety to your application 27 | * **Marry duck typing and static typing** with Python's new Protocol construct 28 | * Learn **design patterns and guidance** for using types in Python code 29 | * And lots more, see the full [course outline](https://training.talkpython.fm/courses/python-type-hint-course-with-hands-on-examples#course_outline). 30 | 31 | ## Learn more 32 | 33 | Visit [the course page at Talk Python](https://training.talkpython.fm/courses/python-type-hint-course-with-hands-on-examples) to learn more and take the course! -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/motorcycle.js: -------------------------------------------------------------------------------- 1 | // To run, in chapter 02. 2 | // 1. npx tsc motorcycle.ts 3 | // 2. node motorcycle.js 4 | var MotorcycleType; 5 | (function (MotorcycleType) { 6 | MotorcycleType["Sport"] = "sport"; 7 | MotorcycleType["Naked"] = "naked"; 8 | MotorcycleType["Touring"] = "touring"; 9 | MotorcycleType["Adventure"] = "adventure"; 10 | MotorcycleType["DualSport"] = "dual_sport"; 11 | MotorcycleType["Motocross"] = "motocross"; 12 | MotorcycleType["CrossCountry"] = "cross_country"; 13 | MotorcycleType["Trail"] = "trail"; 14 | })(MotorcycleType || (MotorcycleType = {})); 15 | var Motorcycle = /** @class */ (function () { 16 | function Motorcycle(model, style, engineSize, offRoad) { 17 | this.model = model; 18 | this.style = style; 19 | this.engineSize = engineSize; 20 | this.offRoad = offRoad; 21 | } 22 | Object.defineProperty(Motorcycle.prototype, "canJump", { 23 | get: function () { 24 | return this.offRoad; 25 | }, 26 | enumerable: false, 27 | configurable: true 28 | }); 29 | Motorcycle.createAdventure = function (model, engineSize) { 30 | var bike = new Motorcycle(model, MotorcycleType.Adventure, engineSize, true); 31 | return bike; 32 | }; 33 | Motorcycle.prototype.toString = function () { 34 | return "".concat(this.model, " ").concat(this.engineSize, ", type: ").concat(this.style, ", off-road: ").concat(this.offRoad, ", can jump: ").concat(this.canJump); 35 | }; 36 | return Motorcycle; 37 | }()); 38 | function createSomeBikes() { 39 | return [ 40 | Motorcycle.createAdventure("Himalayan", 410), 41 | Motorcycle.createAdventure("Tenere", 700), 42 | Motorcycle.createAdventure("KTM", 790), 43 | Motorcycle.createAdventure("Norden", 901) 44 | ]; 45 | } 46 | console.log("Some bikes:"); 47 | var bikes = createSomeBikes(); 48 | for (var _i = 0, bikes_1 = bikes; _i < bikes_1.length; _i++) { 49 | var b = bikes_1[_i]; 50 | console.log(b.toString()); 51 | } 52 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/motorcycle.ts: -------------------------------------------------------------------------------- 1 | // To run, in chapter 02. 2 | // 1. npx tsc motorcycle.ts 3 | // 2. node motorcycle.js 4 | 5 | enum MotorcycleType { 6 | Sport = "sport", 7 | Naked = "naked", 8 | Touring = "touring", 9 | Adventure = "adventure", 10 | DualSport = "dual_sport", 11 | Motocross = "motocross", 12 | CrossCountry = "cross_country", 13 | Trail = "trail" 14 | } 15 | 16 | class Motorcycle { 17 | model: string; 18 | style: MotorcycleType; 19 | engineSize: number; 20 | offRoad: boolean; 21 | 22 | constructor(model: string, style: MotorcycleType, engineSize: number, offRoad: boolean) { 23 | this.model = model; 24 | this.style = style; 25 | this.engineSize = engineSize; 26 | this.offRoad = offRoad; 27 | } 28 | 29 | get canJump(): boolean { 30 | return this.offRoad; 31 | } 32 | 33 | static createAdventure(model: string, engineSize: number): Motorcycle { 34 | const bike = new Motorcycle(model, MotorcycleType.Adventure, engineSize, true); 35 | return bike; 36 | } 37 | 38 | toString(): string { 39 | return `${this.model} ${this.engineSize}, type: ${this.style}, off-road: ${this.offRoad}, can jump: ${this.canJump}`; 40 | } 41 | } 42 | 43 | function createSomeBikes(): Motorcycle[] { 44 | return [ 45 | Motorcycle.createAdventure("Himalayan", 410), 46 | Motorcycle.createAdventure("Tenere", 700), 47 | Motorcycle.createAdventure("KTM", 790), 48 | Motorcycle.createAdventure("Norden", 901) 49 | ]; 50 | } 51 | 52 | console.log("Some bikes:"); 53 | const bikes = createSomeBikes(); 54 | for (const b of bikes) { 55 | console.log(b.toString()); 56 | } 57 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/motorcycles.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | // Run with new project via: 5 | // 1. dotnet new console -o mc -f net7.0 6 | // 2. cd mc && dotnet run 7 | 8 | namespace MotorcycleApp 9 | { 10 | public enum MotorcycleType 11 | { 12 | Sport, 13 | Naked, 14 | Touring, 15 | Adventure, 16 | DualSport, 17 | Motocross, 18 | CrossCountry, 19 | Trail 20 | } 21 | 22 | public class Motorcycle 23 | { 24 | public string Model { get; private set; } 25 | public MotorcycleType Style { get; private set; } 26 | public int EngineSize { get; private set; } 27 | public bool OffRoad { get; private set; } 28 | 29 | public Motorcycle(string model, MotorcycleType style, int engineSize, bool offRoad) 30 | { 31 | Model = model; 32 | Style = style; 33 | EngineSize = engineSize; 34 | OffRoad = offRoad; 35 | } 36 | 37 | public bool CanJump => OffRoad; 38 | 39 | public static Motorcycle CreateAdventure(string model, int engineSize) 40 | { 41 | var bike = new Motorcycle(model, MotorcycleType.Adventure, engineSize, offRoad: true); 42 | return bike; 43 | } 44 | 45 | public override string ToString() 46 | { 47 | return $"{Model} {EngineSize}, type: {Style}, off-road: {OffRoad}, can jump: {CanJump}"; 48 | } 49 | } 50 | 51 | class Program 52 | { 53 | static List CreateSomeBikes() 54 | { 55 | var motorcycles = new List 56 | { 57 | Motorcycle.CreateAdventure("Himalayan", 410), 58 | Motorcycle.CreateAdventure("Tenere", 700), 59 | Motorcycle.CreateAdventure("KTM", 790), 60 | Motorcycle.CreateAdventure("Norden", 901) 61 | }; 62 | 63 | return motorcycles; 64 | } 65 | 66 | static void Main(string[] args) 67 | { 68 | Console.WriteLine("Some bikes:"); 69 | var bikes = CreateSomeBikes(); 70 | foreach (var b in bikes) 71 | { 72 | Console.WriteLine(b); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/motorcycles.swift: -------------------------------------------------------------------------------- 1 | // Run with swiftc motorcycles.swift -o mc 2 | import Foundation 3 | 4 | enum MotorcycleType: String { 5 | case sport 6 | case naked 7 | case touring 8 | case adventure 9 | case dualSport = "dual_sport" 10 | case motocross 11 | case crossCountry 12 | case trail 13 | } 14 | 15 | class Motorcycle { 16 | let model: String 17 | let style: MotorcycleType 18 | let engineSize: Int 19 | let offRoad: Bool? 20 | 21 | init(model: String, style: MotorcycleType, engineSize: Int, offRoad: Bool) { 22 | self.model = model 23 | self.style = style 24 | self.engineSize = engineSize 25 | self.offRoad = offRoad 26 | } 27 | 28 | var canJump: Bool { 29 | return offRoad! 30 | } 31 | 32 | static func createAdventure(model: String, engineSize: Int) -> Motorcycle { 33 | let bike = Motorcycle(model: model, style: .adventure, engineSize: engineSize, offRoad: true) 34 | return bike 35 | } 36 | 37 | var description: String { 38 | return "\(model) \(engineSize), type: \(style.rawValue), off-road: \(offRoad!), can jump: \(canJump)" 39 | } 40 | } 41 | 42 | func createSomeBikes() -> [Motorcycle] { 43 | let motorcycles = [ 44 | Motorcycle.createAdventure(model: "Himalayan", engineSize: 410), 45 | Motorcycle.createAdventure(model: "Tenere", engineSize: 700), 46 | Motorcycle.createAdventure(model: "KTM", engineSize: 790), 47 | Motorcycle.createAdventure(model: "Norden", engineSize: 901) 48 | ] 49 | 50 | return motorcycles 51 | } 52 | 53 | print("Some bikes:") 54 | let bikes = createSomeBikes() 55 | for b in bikes { 56 | print(b.description) 57 | } 58 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-static-vs-dynamic-languages", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "02-static-vs-dynamic-languages", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "typescript": "^5.2.2" 13 | } 14 | }, 15 | "node_modules/typescript": { 16 | "version": "5.2.2", 17 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 18 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 19 | "dev": true, 20 | "bin": { 21 | "tsc": "bin/tsc", 22 | "tsserver": "bin/tsserver" 23 | }, 24 | "engines": { 25 | "node": ">=14.17" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-static-vs-dynamic-languages", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "typescript": "^5.2.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/py-no-types.py: -------------------------------------------------------------------------------- 1 | class Motorcycle: 2 | 3 | def __init__(self, model, style, engine_size, off_road): 4 | self.off_road = off_road 5 | self.engine_size = engine_size 6 | self.style = style 7 | self.model = model 8 | 9 | @property 10 | def can_jump(self): 11 | return bool(self.off_road) 12 | 13 | def __str__(self): 14 | return (f'{self.model} {self.engine_size}, type: {self.style}, ' + 15 | f'off-road: {self.off_road}, can jump: {self.can_jump}') 16 | 17 | @classmethod 18 | def create_adventure(cls, model, engine_size): 19 | return Motorcycle(model, "Adventure", engine_size, True) 20 | 21 | 22 | def create_bikes(): 23 | motorcycles = [ 24 | Motorcycle.create_adventure("Himalayan", 410), 25 | Motorcycle.create_adventure("Ténéré", 700), 26 | Motorcycle.create_adventure("KTM", 790), 27 | Motorcycle.create_adventure("Norden", 901), 28 | ] 29 | 30 | return motorcycles 31 | 32 | 33 | if __name__ == '__main__': 34 | bikes = create_bikes() 35 | 36 | print("Here are some bikes!") 37 | for b in bikes: 38 | print(b) 39 | # Duck type: Motorcycles have a string model and a truthy can_jump. 40 | print(f'The {b.model} can{"" if b.can_jump else "NOT" } jump.') 41 | -------------------------------------------------------------------------------- /code/02-static-vs-dynamic-languages/py-types.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import typing 3 | 4 | 5 | class MotorcycleType(enum.StrEnum): 6 | sport = "sport" 7 | naked = "naked" 8 | touring = "touring" 9 | adventure = "adventure" 10 | dual_sport = "dual_sport" 11 | motocross = "motocross" 12 | cross_country = "cross_country" 13 | trail = "trail" 14 | 15 | 16 | class Motorcycle: 17 | wheel_count: int = 2 18 | 19 | def __init__(self, model: str, style: MotorcycleType, engine_size: int, off_road: bool): 20 | self.off_road: bool = off_road 21 | self.engine_size: int = engine_size 22 | self.style: MotorcycleType = style 23 | self.model: str = model 24 | 25 | @property 26 | def can_jump(self) -> bool: 27 | return self.off_road 28 | 29 | def __str__(self) -> str: 30 | return (f'{self.model} {self.engine_size}, type: {self.style}, ' + 31 | f'off-road: {self.off_road}, can jump: {self.can_jump}') 32 | 33 | @classmethod 34 | def create_adventure(cls, model: str, engine_size: int) -> typing.Self: 35 | return Motorcycle(model, MotorcycleType.adventure, engine_size, True) 36 | 37 | 38 | def create_bikes() -> list[Motorcycle]: 39 | motorcycles = [ 40 | Motorcycle.create_adventure("Himalayan", 410), 41 | Motorcycle.create_adventure("Ténéré", 700), 42 | Motorcycle.create_adventure("KTM", 790), 43 | Motorcycle.create_adventure("Norden", 901), 44 | ] 45 | 46 | return motorcycles 47 | 48 | 49 | if __name__ == '__main__': 50 | bikes: list[Motorcycle] = create_bikes() 51 | 52 | print("Here are some bikes!") 53 | b: Motorcycle # Not really needed. 54 | for b in bikes: 55 | print(b, b.wheel_count) 56 | -------------------------------------------------------------------------------- /code/03-typing-in-python/calculator.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | return x + y 3 | -------------------------------------------------------------------------------- /code/03-typing-in-python/calculator.pyi: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | RealNumber = Union[int, float] 4 | 5 | # def add(x: int, y: int) -> int: ... 6 | # def add(x: int|float, y: int|float) -> int|float: ... 7 | # def add(x: RealNumber, y: RealNumber) -> RealNumber: ... 8 | def add(x: float, y: float) -> float: ... 9 | -------------------------------------------------------------------------------- /code/03-typing-in-python/p1_variables.py: -------------------------------------------------------------------------------- 1 | # ########################################## 2 | # 3 | # syntax of "x: type" ********************* 4 | # 5 | import typing 6 | from typing import Optional 7 | 8 | x = 7 9 | y: int = 10 # Explicitly declare the type as int. 10 | 11 | print(x, y) # Just to clear the editor warnings. 12 | 13 | x = "Happy numbers" 14 | y = "Sad numbers" # Error in the editor. 15 | 16 | print(x, y) 17 | 18 | # ########################################## 19 | # 20 | # Survey of core types ********************* 21 | # 22 | 23 | u: int = 27 24 | v: float = 1.4121356 25 | c: complex = complex(0, -v) 26 | text: str = "Some text" 27 | b: bytes = b"Bytes text" 28 | truth: bool = True 29 | lst: list = [1, 1, 2, 3, 5, 8] 30 | s: set = {1, 1, 2, 3, 5, 8} # 1,2,3,5,8 31 | 32 | print(u, v, c, text, b, truth, lst, s) 33 | 34 | lst = s # <- Error 35 | 36 | # ########################################## 37 | # 38 | # Nullable types ********************* 39 | # 40 | 41 | z: int = 42 42 | print(z) 43 | 44 | z = None # <- Error! 45 | 46 | z2: str = "Never nothing!" 47 | print(z2) 48 | z2 = None # <- Error! 49 | 50 | z3: typing.Optional[int] = 43 51 | print(z3) 52 | z3: Optional[int] = 43 53 | z4: int | None = 43 54 | print(z3) 55 | z3 = None 56 | print(z3) 57 | z3 = 44 58 | print(z3) 59 | z3 = "ABC" # <- Error! 60 | 61 | # ########################################## 62 | # 63 | # Unions ********************* 64 | # 65 | 66 | un1: typing.Union[int, str] = 1 67 | un2: int | str = 2 68 | un3: int | str = "Three" 69 | 70 | print(un1 + un2) 71 | 72 | un1 = "One" 73 | un2 = "Two" 74 | print(un1 + un2) 75 | 76 | un1 = [1, 1, 2] # <- Error! 77 | 78 | # un1.casefold() 79 | 80 | # ########################################## 81 | # 82 | # What if you don't know the type? ********* 83 | # 84 | 85 | unknown: typing.Any = 78 86 | # unknown: Any = 78 87 | print(unknown) 88 | 89 | unknown = "Seventy Eight!" 90 | print(unknown) 91 | 92 | unknown = {7, 8, 8} 93 | print(unknown) 94 | 95 | # ########################################## 96 | # 97 | # Constants ********************* 98 | # 99 | 100 | not_const_1 = "Some value" 101 | print(not_const_1) 102 | 103 | not_const_1 = "Other value" 104 | 105 | CONST_2 = "Fixed value" # Implicit, conventional constant 106 | print(CONST_2) 107 | CONST_2 = "No longer fixed!" # Should have a warning, I guess? 108 | 109 | CONST_3: typing.Final = "Really a constant, sorta" # Explicit constant in the type system. 110 | print(CONST_3) 111 | 112 | CONST_3 = "This should not change!" # <- Error! In the type system. 113 | print(CONST_3) 114 | 115 | # ########################################## 116 | # 117 | # Beware Little Bobby Tables **************** 118 | # https://peps.python.org/pep-0675/ 119 | # 120 | 121 | # Old way: 122 | # student_name: str = input("What is the student's name?") 123 | # student_name: str = "Robert Tables" 124 | # query: str = f"SELECT * FROM Students WHERE name ='{student_name}'" 125 | # print(query) 126 | # 127 | # student_name = "'; DROP TABLE Students; --" 128 | # query = f"SELECT * FROM Students WHERE name ='{student_name}'" 129 | # print(query) 130 | 131 | # New way: 132 | # student_name: str = input("What is the student's name?") 133 | student_name: typing.LiteralString = "Robert Tables" 134 | query: typing.LiteralString = f"SELECT * FROM Students WHERE name ='{student_name}'" 135 | print(query) 136 | 137 | student_name = "'; DROP TABLE Students; --" 138 | query = f"SELECT * FROM Students WHERE name ='{student_name}'" 139 | print(query) 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /code/03-typing-in-python/p2_functions.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Optional 3 | 4 | 5 | def main(): 6 | print("Typing functions") 7 | print() 8 | 9 | n1 = fib(1) 10 | n2 = fib(2) 11 | n3 = fib(3) 12 | n4 = fib(4) 13 | 14 | print(n1, n2, n3, n4) 15 | 16 | o1 = fib_small(-1) 17 | o2 = fib_small(5) 18 | print(o1, o2) 19 | 20 | say_hello("Michael") 21 | x = say_hello("Sam") 22 | print(type(x), x) 23 | 24 | use_function_explicit(lambda p1, p2: print(f"{p1}'s favorite number is {p2}.")) 25 | # use_function(lambda a: a*a) # <- Error and a crash. 26 | use_function_explicit(usable_func1) 27 | # use_function_explicit(usable_func2) # <- Error and a crash. 28 | 29 | 30 | def usable_func1(name: str, num: int) -> None: 31 | print(f"{name}'s common number is {num}.") 32 | 33 | 34 | def usable_func2(name: str) -> None: 35 | print(f"Hello {name}") 36 | 37 | 38 | def use_function_explicit(f: typing.Callable[[str, int], None]): 39 | f("Michael", 42) 40 | 41 | 42 | def use_function(f): 43 | f("Michael", 42) 44 | 45 | 46 | def fib(n: int) -> int: 47 | current, nxt = 0, 1 48 | 49 | for _ in range(n): 50 | current, nxt = nxt, nxt + current 51 | 52 | return current 53 | 54 | 55 | def fib_small(n: int) -> Optional[int]: 56 | if n <= 0: 57 | return None 58 | 59 | return fib(n) 60 | 61 | 62 | def say_hello(name: str) -> None: 63 | print(f'Hello there {name}!') 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /code/03-typing-in-python/p3_collections.py: -------------------------------------------------------------------------------- 1 | # ########################################## 2 | # 3 | # Python allows for heterogeneous types all smushed together 4 | # 5 | import typing 6 | from typing import Any 7 | 8 | 9 | class Person: 10 | def __init__(self, name: str, number: int): 11 | self.name = name 12 | self.number = number 13 | 14 | 15 | # Person.ssn = 434 # Naaaw 16 | 17 | # Also, probably not: 18 | things: list[Any] = [ 19 | 7, 20 | Person("Michael", 42), 21 | 7.14, 22 | "Seven", 23 | "Seize the day!", 24 | [1, 1, 2, 3], 25 | ] 26 | 27 | for t in things: 28 | print(t) 29 | 30 | # Collections, the "right" way: 31 | # from typing import List 32 | # numbers: List[int] = [2, 3, 5, 7, 11, 13, 17, ] # Works in 3.5+ 33 | numbers: list[int] = [2, 3, 5, 7, 11, 13, 17, ] # 3.9+ 34 | people: list[Person] = [ 35 | Person("Michael", 42), 36 | Person("Sarah", 3), 37 | Person("Zoe", 100), 38 | ] 39 | 40 | print(numbers[0].to_bytes()) # Autocomplete 41 | print(people[0].name) 42 | 43 | # ########################################## 44 | # 45 | # More complex data structures: 46 | # 47 | 48 | user_lookup_by_id: dict[int, Person] = { 49 | p.number: p 50 | for p in people 51 | } 52 | 53 | u1 = user_lookup_by_id.get("Seven") # <- Error, wrong key type 54 | u2: str = user_lookup_by_id.get(42) # <- Error, not a str. 55 | print(u2) 56 | u3 = user_lookup_by_id.get(3) 57 | u4: Person = user_lookup_by_id.get(3) # <- Bug, should be Optional[Person] 58 | 59 | t: typing.Tuple[int, int, Person, str] = (7, 7, Person(1, 2), "") 60 | 61 | def get_data() -> typing.Tuple[int, int, Person, str]: 62 | return 7, 7, Person(1, 2), "" 63 | 64 | 65 | t2 = get_data() 66 | # t2[2].name 67 | -------------------------------------------------------------------------------- /code/03-typing-in-python/p4_classes.py: -------------------------------------------------------------------------------- 1 | # See code/02-static-vs-dynamic-languages/py-types.py for details 2 | -------------------------------------------------------------------------------- /code/03-typing-in-python/p5_external_types.py: -------------------------------------------------------------------------------- 1 | import calculator 2 | 3 | 4 | def main(): 5 | s = calculator.add(1, 3) 6 | s2 = calculator.add(1.1, 3) 7 | print(s, s2) 8 | 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/d1_parsing_with_pydantic.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pydantic 4 | 5 | 6 | class Weather(pydantic.BaseModel): 7 | temp: float 8 | location: Optional[str] = None 9 | pressure: int 10 | humidity: int 11 | temp_range: list[int] 12 | 13 | 14 | def main(): 15 | data = { 16 | "temp": 60.44, 17 | # "location": "Portland, OR", 18 | # "location": ("Portland", "OR"), # <-- Error! 19 | "pressure": 1019.0, 20 | "humidity": 86, 21 | "temp_range": [56, "64"] 22 | } 23 | 24 | w = Weather(**data) 25 | print(w) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/d2_parsing_weather.py: -------------------------------------------------------------------------------- 1 | from weather_models import WeatherForecast 2 | 3 | def main(): 4 | data = { 5 | "weather": 6 | { 7 | "description": "broken clouds", 8 | "category": "Clouds" 9 | }, 10 | "wind": 11 | { 12 | "speed": 5.99, 13 | "deg": 0 14 | }, 15 | "units": "imperial", 16 | "forecast": 17 | { 18 | "temp": 60.44, 19 | "feels_like": 60.22, 20 | "pressure": 1019, 21 | "humidity": 86, 22 | "low": 56, 23 | "high": "64" 24 | }, 25 | "location": 26 | { 27 | "city": "Portland", 28 | "state": "OR", 29 | "country": "US" 30 | }, 31 | "rate_limiting": 32 | { 33 | "unique_lookups_remaining": 49, 34 | "lookup_reset_window": "1 hour" 35 | } 36 | } 37 | 38 | w = WeatherForecast(**data) 39 | print(w) 40 | 41 | print(f"Right now it's {w.weather.description} and {w.forecast.temp:.0f} F degrees.") 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/weather_models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Weather(BaseModel): 5 | description: str 6 | category: str 7 | 8 | 9 | class Wind(BaseModel): 10 | speed: float 11 | deg: int 12 | 13 | 14 | class Forecast(BaseModel): 15 | temp: float 16 | feels_like: float 17 | pressure: int 18 | humidity: int 19 | low: int 20 | high: int 21 | 22 | 23 | class Location(BaseModel): 24 | city: str 25 | state: str 26 | country: str 27 | 28 | 29 | class RateLimiting(BaseModel): 30 | unique_lookups_remaining: int 31 | lookup_reset_window: str 32 | 33 | 34 | class WeatherForecast(BaseModel): 35 | weather: Weather 36 | wind: Wind 37 | units: str 38 | forecast: Forecast 39 | location: Location 40 | rate_limiting: RateLimiting 41 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/IMPORT_DATA.md: -------------------------------------------------------------------------------- 1 | # Data for the course 2 | 3 | This course uses custom data from [PyPI](https://pypi.org) sources. 4 | 5 | Start by **installing MongoDB** ([steps](https://www.mongodb.com/docs/manual/administration/install-community/)) and **MongoDB Management Tools** ([steps](https://www.mongodb.com/docs/database-tools/installation/installation/)). Start the MongoDB server running as well as the management tools binaries in the path. 6 | 7 | **Download the data** from Talk Python at: 8 | 9 | [https://talkpython.fm/pypi5k](https://talkpython.fm/pypi5k) 10 | 11 | Unzip the data temporarily wherever you like. Let's assume the downloaded `pypi-5k-mongodb-dump.zip` file is in `~/Downloads/`. Then once you uncompress it, there will be a `pypi` folder with many `*.bson` and `*.json` files. **In that directory** (e.g. `~/Downloads/pypi`), run this command: 12 | 13 | ```bash 14 | mongorestore --drop --db pypi ./ 15 | ``` 16 | 17 | You should see output roughly analogous to: 18 | 19 | ![](./mongorestore-output.png) 20 | 21 | Note that we did not restore all the indexes you would want for this data. Rather, that will be added when we get to the performance section of the course. 22 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/code/04-frameworks-built-on-typing/web_example/__init__.py -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/code/04-frameworks-built-on-typing/web_example/api/__init__.py -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/code/04-frameworks-built-on-typing/web_example/api/models/__init__.py -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/models/recent_package_model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pydantic 4 | 5 | 6 | class RecentPackage(pydantic.BaseModel): 7 | name: str 8 | updated: datetime.datetime 9 | 10 | 11 | class RecentPackagesModel(pydantic.BaseModel): 12 | count: int 13 | packages: list[RecentPackage] 14 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/models/stats_model.py: -------------------------------------------------------------------------------- 1 | import pydantic 2 | 3 | 4 | class StatsModel(pydantic.BaseModel): 5 | user_count: int 6 | package_count: int 7 | release_count: int 8 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/package_api.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | 3 | from api.models.recent_package_model import RecentPackagesModel, RecentPackage 4 | from models.package import Package 5 | from services import package_service 6 | 7 | router = fastapi.APIRouter() 8 | 9 | 10 | @router.get('/api/packages/recent/{count}', response_model=RecentPackagesModel) 11 | async def recent(count: int): 12 | count = max(1, count) 13 | packages = await package_service.recently_updated(count) 14 | 15 | package_models = [ 16 | RecentPackage(name=p.id, updated=p.last_updated) 17 | for p in packages 18 | ] 19 | 20 | model = RecentPackagesModel(count=count, packages=package_models) 21 | return model 22 | 23 | 24 | @router.get('/api/packages/details/{name}', response_model=Package) 25 | async def details(name: str): 26 | package = await package_service.package_by_name(name) 27 | if package is None: 28 | return fastapi.responses.JSONResponse({'error': f'Package {name} not found'}, status_code=404) 29 | 30 | return package 31 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/api/stats_api.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | 3 | from api.models.stats_model import StatsModel 4 | from services import package_service, user_service 5 | 6 | router = fastapi.APIRouter() 7 | 8 | 9 | @router.get('/api/stats', response_model=StatsModel) 10 | async def stats(): 11 | packages = await package_service.package_count() 12 | releases = await package_service.release_count() 13 | users = await user_service.user_count() 14 | 15 | model = StatsModel(user_count=users, package_count=packages, release_count=releases) 16 | return model 17 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/infrastructure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/code/04-frameworks-built-on-typing/web_example/infrastructure/__init__.py -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/infrastructure/mongo_setup.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import beanie 4 | import motor.motor_asyncio 5 | 6 | import models 7 | 8 | 9 | async def init_connection(database: str, server: Optional[str] = 'localhost', 10 | port: int = 27017, username: Optional[str] = None, password: Optional[str] = None, 11 | use_ssl: bool = False): 12 | server = server or 'localhost' 13 | port = port or 27017 14 | 15 | await _motor_init(database=database, password=password, port=port, server=server, 16 | use_ssl=use_ssl, username=username, models_classes=models.all_models) 17 | 18 | 19 | async def _motor_init(database: str, password: Optional[str], port: int, server: str, 20 | use_ssl: bool, username: Optional[str], models_classes): 21 | conn_string = create_connection_string(password, port, server, use_ssl, username) 22 | 23 | print(f'Initializing motor connection for db {database} on {server}:{port}') 24 | print(f'Connection string: {conn_string.replace(password or "NO_PASSWORD", "***********")}') 25 | 26 | # Crete Motor client 27 | client = motor.motor_asyncio.AsyncIOMotorClient(conn_string) 28 | 29 | # Init beanie with the Product document class 30 | await beanie.init_beanie(database=client[database], document_models=models_classes) 31 | print(f"Init done for db {database}") 32 | 33 | 34 | def create_connection_string(password, port, server, use_ssl, username): 35 | if username or password: 36 | use_ssl = str(use_ssl).lower() 37 | return f"mongodb://{username}:{password}@{server}:{port}/?authSource=admin&tls={use_ssl}&tlsInsecure=true" 38 | else: 39 | return f"mongodb://{server}:{port}" 40 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/infrastructure/time_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | 5 | def seconds_to_duration_text(duration_in_sec): 6 | if duration_in_sec < 60: 7 | txt = f"0:{str(duration_in_sec).zfill(2)}" 8 | return txt 9 | 10 | if duration_in_sec < 60 * 60: 11 | mins = int(duration_in_sec / 60) 12 | sec = duration_in_sec % 60 13 | txt = f"{str(mins).zfill(2)}:{str(sec).zfill(2)}" 14 | if mins > 0: 15 | txt = txt.lstrip('0') 16 | return txt 17 | 18 | hrs = int(duration_in_sec / 60 / 60) 19 | mins = int(duration_in_sec / 60) 20 | sec = duration_in_sec % 60 21 | txt = f"{hrs}:{str(mins - hrs * 60).zfill(2)}:{str(sec).zfill(2)}" 22 | if hrs > 0: 23 | txt = txt.lstrip('0') 24 | return txt 25 | 26 | 27 | def month_name_from_date(date) -> str: 28 | months = { 29 | 1: "Jan", 30 | 2: "Feb", 31 | 3: "March", 32 | 4: "April", 33 | 5: "May", 34 | 6: "June", 35 | 7: "July", 36 | 8: "Aug", 37 | 9: "Sept", 38 | 10: "Oct", 39 | 11: "Nov", 40 | 12: "Dec", 41 | } 42 | return months[date.month] 43 | 44 | 45 | def date_suffix(date: datetime.datetime) -> str: 46 | day = date.day % 10 47 | if day == 1: 48 | return "st" 49 | elif day == 2: 50 | return "nd" 51 | elif day == 3: 52 | return "rd" 53 | else: 54 | return "th" # noqa: FURB126 55 | 56 | 57 | class timed_block: 58 | def __init__(self, message: str): 59 | self.message = message 60 | self.t0 = None 61 | self.dt = None 62 | 63 | def __enter__(self): 64 | self.t0 = datetime.datetime.now() 65 | 66 | def __exit__(self, exc_type, exc_val, exc_tb): 67 | self.dt = datetime.datetime.now() - self.t0 68 | print(f'{self.message}. Complete in {self.dt.total_seconds() * 1000:,} ms.') 69 | 70 | 71 | def timed(method): 72 | @wraps(method) 73 | def timed_method(*args, **kw): 74 | t0 = datetime.datetime.now() 75 | result = method(*args, **kw) 76 | t1 = datetime.datetime.now() 77 | 78 | dt = t1 - t0 79 | print(f" *** timings: {method.__name__} ran in {dt.total_seconds() * 1000:,.3f} ms") 80 | 81 | return result 82 | 83 | return timed_method 84 | 85 | 86 | def timed_async(method): 87 | async def timed_method(*args, **kw): 88 | t0 = datetime.datetime.now() 89 | result = await method(*args, **kw) 90 | t1 = datetime.datetime.now() 91 | 92 | dt = t1 - t0 93 | print(f" *** timings: {method.__name__} ran in {dt.total_seconds() * 1000:,.3f} ms") 94 | 95 | return result 96 | 97 | return timed_method 98 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/main.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | import uvicorn 3 | from fastapi.templating import Jinja2Templates 4 | from starlette.requests import Request 5 | from starlette.staticfiles import StaticFiles 6 | 7 | from api import package_api 8 | from api import stats_api 9 | from infrastructure import mongo_setup 10 | 11 | api = fastapi.FastAPI() 12 | templates = Jinja2Templates(directory='templates') 13 | 14 | 15 | def main(): 16 | configure_routing() 17 | 18 | # NO! Use @api.on_event('startup') 19 | # asyncio.run(mongo_setup.init_connection('pypi')) 20 | 21 | uvicorn.run(api) 22 | 23 | 24 | def configure_routing(): 25 | api.mount('/static', StaticFiles(directory='static'), name='static') 26 | api.include_router(package_api.router) 27 | api.include_router(stats_api.router) 28 | 29 | 30 | @api.on_event('startup') 31 | async def configure_db(): 32 | await mongo_setup.init_connection('pypi') 33 | 34 | 35 | @api.get('/', include_in_schema=False) 36 | def index(request: Request): 37 | return templates.TemplateResponse('index.html', {"name": "The app!", "request": request}) 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | else: 43 | configure_routing() 44 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/models/__init__.py: -------------------------------------------------------------------------------- 1 | from models import package, user, release_analytics 2 | 3 | all_models = [ 4 | package.Package, 5 | user.User, 6 | release_analytics.ReleaseAnalytics 7 | ] 8 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/models/package.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | import beanie as beanie 5 | import pydantic 6 | import pymongo 7 | 8 | 9 | class Release(pydantic.BaseModel): 10 | major_ver: int 11 | minor_ver: int 12 | build_ver: int 13 | created_date: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) 14 | comment: Optional[str] = None 15 | url: Optional[str] = None 16 | size: int 17 | 18 | 19 | class Package(beanie.Document): 20 | id: str 21 | created_date: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) 22 | last_updated: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) 23 | summary: str 24 | description: str 25 | home_page: Optional[str] = None 26 | docs_url: Optional[str] = None 27 | package_url: Optional[str] = None 28 | author_name: Optional[str] = None 29 | author_email: Optional[str] = None 30 | license: Optional[str] = None 31 | releases: list[Release] 32 | maintainer_ids: list[beanie.PydanticObjectId] 33 | 34 | class Settings: 35 | name = 'packages' 36 | indexes = [ 37 | pymongo.IndexModel(keys=[('last_updated', pymongo.DESCENDING)], name='last_updated_descending'), 38 | pymongo.IndexModel(keys=[("created_date", pymongo.ASCENDING)], name="created_date_ascend"), 39 | pymongo.IndexModel(keys=[("releases.created_date", pymongo.ASCENDING)], 40 | name="releases_created_date_ascend"), 41 | pymongo.IndexModel(keys=[("author_email", pymongo.ASCENDING)], name="author_email_ascend"), 42 | 43 | pymongo.IndexModel(keys=[("releases.major_ver", pymongo.ASCENDING)], name="releases_major"), 44 | pymongo.IndexModel(keys=[("releases.minor_ver", pymongo.ASCENDING)], name="releases_minor"), 45 | pymongo.IndexModel(keys=[("releases.build_ver", pymongo.ASCENDING)], name="releases_build"), 46 | 47 | pymongo.IndexModel(keys=[ 48 | ("releases.major_ver", pymongo.ASCENDING), 49 | ("releases.minor_ver", pymongo.ASCENDING), 50 | ("releases.build_ver", pymongo.ASCENDING) 51 | ], 52 | name="releases_version_ascending"), 53 | ] 54 | 55 | 56 | class PackageTopLevelOnly(pydantic.BaseModel): 57 | id: str 58 | last_updated: datetime.datetime 59 | summary: str 60 | 61 | class Settings: 62 | projection = { 63 | "id": "$_id", 64 | "summary": "$summary", 65 | "last_updated": "$last_updated", 66 | } 67 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/models/release_analytics.py: -------------------------------------------------------------------------------- 1 | import beanie 2 | 3 | 4 | class ReleaseAnalytics(beanie.Document): 5 | total_releases: int 6 | 7 | class Settings: 8 | name = 'release_analytics' 9 | indexes = [] 10 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/models/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | import beanie 5 | import pydantic 6 | import pymongo 7 | 8 | 9 | class Location(pydantic.BaseModel): 10 | state: Optional[str] = None 11 | country: Optional[str] = None 12 | 13 | 14 | class User(beanie.Document): 15 | name: str 16 | email: str 17 | hash_password: Optional[str] = None 18 | created_date: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) 19 | last_login: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) 20 | profile_image_url: Optional[str] = None 21 | location: Location 22 | 23 | class Settings: 24 | name = 'users' 25 | indexes = [ 26 | pymongo.IndexModel(keys=[("created_date", pymongo.DESCENDING)], name="created_date_descend"), 27 | pymongo.IndexModel(keys=[("last_login", pymongo.DESCENDING)], name="last_login_descend"), 28 | 29 | pymongo.IndexModel(keys=[("email", pymongo.ASCENDING)], name="email_ascend", unique=True), 30 | ] 31 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/code/04-frameworks-built-on-typing/web_example/services/__init__.py -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/services/package_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | import pymongo.results 5 | from beanie.odm.operators.find.array import ElemMatch 6 | from beanie.odm.operators.update import array 7 | from beanie.odm.operators.update.general import Set, Inc 8 | 9 | from models.package import Package, Release, PackageTopLevelOnly 10 | from models.release_analytics import ReleaseAnalytics 11 | 12 | 13 | async def package_count() -> int: 14 | return await Package.count() 15 | 16 | 17 | async def release_count() -> int: 18 | analytics = await ReleaseAnalytics.find_one() 19 | if not analytics: 20 | print("ERROR: No analytics?") 21 | return 0 22 | 23 | return analytics.total_releases 24 | 25 | 26 | async def recently_updated(count: int = 5) -> list[Package]: 27 | # noinspection PyUnresolvedReferences 28 | updated = await Package.find_all().sort(-Package.last_updated).limit(count).to_list() 29 | 30 | return updated 31 | 32 | 33 | async def package_by_name(name: str, summary_only=False) -> Optional[Package] | Optional[PackageTopLevelOnly]: 34 | if not name: 35 | return None 36 | 37 | name = name.lower().strip() 38 | 39 | query = Package.find_one(Package.id == name) 40 | 41 | if not summary_only: 42 | return await query 43 | else: 44 | return await query.project(PackageTopLevelOnly) 45 | 46 | 47 | async def packages_with_version(major: int, minor: int, build: int) -> int: 48 | # noinspection PyUnresolvedReferences 49 | # Oops, not exactly! 50 | # packages_count = await Package.find( 51 | # Package.releases.major_ver == major,# 1 52 | # Package.releases.minor_ver == minor,# 2 53 | # Package.releases.build_ver == build, 54 | # ).count() 55 | 56 | packages_count = await Package.find( 57 | ElemMatch( 58 | Package.releases, 59 | {"major_ver": major, "minor_ver": minor, "build_ver": build} 60 | ) 61 | ).count() 62 | 63 | return packages_count 64 | 65 | 66 | async def create_release(major: int, minor: int, build: int, 67 | name: str, comment: str, size: int, url: Optional[str]): 68 | release = Release( 69 | major_ver=major, minor_ver=minor, build_ver=build, 70 | comment=comment, url=url, size=size 71 | ) 72 | 73 | update_result: pymongo.results.UpdateResult = await Package \ 74 | .find_one(Package.id == name).update( 75 | array.Push({Package.releases: release}), 76 | Set({Package.last_updated: datetime.datetime.now()}) 77 | ) 78 | 79 | if update_result.modified_count < 1: 80 | raise Exception(f"No package with {name}") 81 | 82 | await ReleaseAnalytics.find_one().update(Inc({ReleaseAnalytics.total_releases: 1})) 83 | 84 | # Fine but full ODM style, less efficient 85 | # async def create_release(major: int, minor: int, build: int, 86 | # name: str, comment: str, size: int, url: Optional[str]): 87 | # package = await package_by_name(name) 88 | # if package is None: 89 | # raise Exception(f"No package with {name}") 90 | # 91 | # release = Release( 92 | # major_ver=major, minor_ver=minor, build_ver=build, 93 | # comment=comment, url=url, size=size 94 | # ) 95 | # 96 | # package.last_updated = datetime.datetime.now() 97 | # package.releases.append(release) 98 | # await package.save() 99 | # 100 | # analytics = await ReleaseAnalytics.find_one() 101 | # analytics.total_releases += 1 102 | # await analytics.save() 103 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/services/user_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | # why argon? 4 | # https://research.redhat.com/blog/article/how-expensive-is-it-to-crack-a-password-derived-with-argon2-very/ 5 | from passlib.handlers.argon2 import argon2 as crypto 6 | 7 | from models.user import User, Location 8 | 9 | crypto.default_rounds = 25 # about 225ms of work 10 | 11 | 12 | async def user_count() -> int: 13 | return await User.count() 14 | 15 | 16 | async def create_user(name: str, email: str, password: str, 17 | profile_image_url: Optional[str], location: Location) -> User: 18 | email = email.lower().strip() 19 | name = name.strip() 20 | password = password.strip() 21 | 22 | if await user_by_email(email): 23 | raise Exception(f"User already exists with {email}.") 24 | 25 | hash_password = crypto.encrypt(password) 26 | user = User(name=name, email=email, hash_password=hash_password, 27 | profile_image_url=profile_image_url, location=location) 28 | 29 | await user.save() 30 | 31 | return user 32 | 33 | 34 | async def user_by_email(email: str) -> Optional[User]: 35 | email = email.lower().strip() 36 | return await User.find_one(User.email == email) 37 | -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/static/css/site.css: -------------------------------------------------------------------------------- 1 | a:visited { color: blue; } 2 | 3 | body { 4 | background-color: #bac5ce; 5 | } -------------------------------------------------------------------------------- /code/04-frameworks-built-on-typing/web_example/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PyPI Demo API 6 | 7 | 8 | 9 |

PyPI Demo API

10 | 11 |

12 | Not much to see here yet. Visit:
13 |
14 |

15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /code/05-tooling/my_test.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Final 2 | 3 | x: str = "ABC" 4 | z: Final[int] = 101 5 | 6 | 7 | def print_a_number(num: int) -> list: 8 | print(num) 9 | another(1, 7) 10 | return None 11 | 12 | 13 | def other(w): 14 | print(w.upper()) 15 | 16 | 17 | print_a_number(x) 18 | print_a_number(z) 19 | other(x) 20 | other(z) 21 | 22 | z = 102 23 | 24 | 25 | def another(u, v) -> Any: 26 | return u + v 27 | -------------------------------------------------------------------------------- /code/05-tooling/mypy.ini: -------------------------------------------------------------------------------- 1 | # Global options: 2 | 3 | [mypy] 4 | warn_return_any = True 5 | warn_unused_configs = True 6 | 7 | # Per-module options: 8 | 9 | ;[mypy-mytest] 10 | ;ignore_missing_imports = True 11 | -------------------------------------------------------------------------------- /code/05-tooling/runtime_example.py: -------------------------------------------------------------------------------- 1 | from beartype import beartype 2 | 3 | 4 | class Point: 5 | def __init__(self, value: float): 6 | self.value = value 7 | 8 | 9 | def main(): 10 | # x = input("Enter the first number: ") 11 | # y = input("Enter the second number: ") 12 | x: float = float(input("Enter the first number: ")) 13 | y: float = float(input("Enter the second number: ")) 14 | 15 | z = math_on_numbers(x, y) 16 | print(f"The result is {z}.") 17 | 18 | p1 = Point(x) 19 | p2 = Point(y) 20 | z = math_on_points(p1, p2) 21 | # z = math_on_points(p1, y) 22 | print(f"The result is {z}.") 23 | 24 | 25 | @beartype 26 | def math_on_numbers(x: float, y: float): 27 | return x * x + 3 * y 28 | 29 | 30 | @beartype 31 | def math_on_points(x: Point, y: Point): 32 | return x.value * x.value + 3 * y.value 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /code/05-tooling/runtime_speed.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from timd import timed_function 4 | 5 | 6 | def main(): 7 | totals = collector_top([100, 20, 60]) 8 | print(f"Done with {totals:,} actions.") 9 | 10 | 11 | @timed_function 12 | def collector_top(counts: list[int]) -> int: 13 | total: int = 1 14 | 15 | for count in counts: 16 | for _ in range(count): 17 | total += collect_part1(int(count / 5)) 18 | total += collect_part2(int(count / 2)) 19 | 20 | return total 21 | 22 | 23 | def collect_part1(count: int) -> int: 24 | total: int = max(count, 1) 25 | groups = [] 26 | 27 | for _ in range(count): 28 | for _ in range(10_000): 29 | groups.append(math.sqrt(count)) 30 | 31 | return total 32 | 33 | 34 | def collect_part2(count: int) -> int: 35 | total: int = max(count, 1) 36 | groups = [] 37 | for _ in range(10_000): 38 | groups.append("This is a common sentence. " * 100) 39 | 40 | return total 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /code/05-tooling/runtime_speed_checked.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from beartype import beartype 4 | 5 | from timd import timed_function 6 | 7 | 8 | def main(): 9 | totals = collector_top([100, 20, 60]) 10 | print(f"Done with {totals:,} actions.") 11 | 12 | 13 | @timed_function 14 | def collector_top(counts: list[int]) -> int: 15 | total: int = 1 16 | 17 | for count in counts: 18 | for _ in range(count): 19 | total += collect_part1(int(count / 5)) 20 | total += collect_part2(int(count / 2)) 21 | 22 | return total 23 | 24 | 25 | @beartype 26 | def collect_part1(count: int) -> int: 27 | total: int = 0 28 | groups = [] 29 | 30 | for _ in range(count): 31 | total += 1 32 | math_stuff(count, groups) 33 | 34 | return total 35 | 36 | 37 | @beartype 38 | def math_stuff(count: int, groups: list[float]): 39 | for _ in range(10_000): 40 | groups.append(math.sqrt(count)) 41 | 42 | 43 | @beartype 44 | def collect_part2(count: int) -> int: 45 | total: int = 1 46 | groups = [] 47 | str_stuff(groups) 48 | 49 | return total 50 | 51 | 52 | @beartype 53 | def str_stuff(groups: list[str]): 54 | for _ in range(10_000): 55 | groups.append("This is a common sentence. " * 100) 56 | 57 | 58 | if __name__ == '__main__': 59 | main() 60 | -------------------------------------------------------------------------------- /code/05-tooling/timd.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | 5 | def timed_function(func): 6 | @wraps(func) 7 | def time_it_inner(*args, **kwargs): 8 | t0 = datetime.datetime.now() 9 | 10 | try: 11 | return func(*args, **kwargs) 12 | finally: 13 | dt = datetime.datetime.now() - t0 14 | print(f"Timed function done in {dt.total_seconds() * 1000:,.1f} ms.") 15 | 16 | return time_it_inner 17 | -------------------------------------------------------------------------------- /code/06-orthogonal-typing/protocol_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | # We COULD create an automobile base-class 3 | # Wait, are motorcycles autos? or different? 4 | # Well, we can drive both, that's good enough 5 | # So it'll have engines, transmissions, key to turn on, throttle, brakes 6 | # Wait, there are electric motorcycles and cars. I guess they still have transmissions (or do they?) 7 | # Wait, there are off-road motorcycles, they don't have keys, but do have transmissions. 8 | # Wait, we could drive a mech/robot thing too. But they have no keys, not throttle or brakes! 9 | # https://bostondynamics.com 10 | # Ugh, OOP is hard. I just want something I can turn on, drive to a location/direction, and stop. 11 | # Enter protocols! 12 | 13 | """ 14 | 15 | # ############################ 16 | # 17 | # Back to our motorcycle example from chapter 2, again 18 | # 19 | 20 | 21 | import enum 22 | import random 23 | import typing 24 | 25 | 26 | # These should: 27 | # turn_on 28 | # turn_towards a direction 29 | # accelerate at a rate 30 | 31 | class Drivable(typing.Protocol): 32 | def turn_on(self) -> bool: ... 33 | 34 | def turn_towards(self, direction: str) -> None: ... 35 | 36 | def accelerate(self, rate: float) -> None: ... 37 | 38 | 39 | def main(): 40 | mc: Motorcycle = Motorcycle.create_adventure("Tènèrè", 700) 41 | do_vehicle_things(mc) 42 | 43 | 44 | def do_vehicle_things(vehicle: Drivable): 45 | print("Doing vehicle things!") 46 | vehicle.turn_on() 47 | vehicle.turn_towards("North") 48 | vehicle.accelerate(9.81) 49 | 50 | 51 | class MotorcycleType(enum.StrEnum): 52 | sport = "sport" 53 | naked = "naked" 54 | touring = "touring" 55 | adventure = "adventure" 56 | dual_sport = "dual_sport" 57 | motocross = "motocross" 58 | cross_country = "cross_country" 59 | trail = "trail" 60 | 61 | 62 | class Motorcycle: 63 | 64 | def __init__(self, model: str, style: MotorcycleType, engine_size: int, off_road: bool): 65 | self.model: str = model 66 | self.style: MotorcycleType = style 67 | self.engine_size: int = engine_size 68 | self.off_road: bool = off_road 69 | 70 | # Drivable protocol methods: 71 | 72 | def turn_on(self) -> bool: 73 | on = random.randint(0, 5) % 5 != 0 74 | print(f'The {self.model} is now {"running" if on else "stalled"}.') 75 | return on 76 | 77 | def turn_towards(self, direction: str) -> None: 78 | print(f'The {self.model} turns towards {direction}.') 79 | return 80 | 81 | def accelerate(self, rate: float) -> None: 82 | print(f'The {self.model} is now going {rate:,.2f} faster.') 83 | return 84 | 85 | @property 86 | def can_jump(self) -> bool: 87 | return self.off_road 88 | 89 | @classmethod 90 | def create_adventure(cls, model: str, engine_size: int) -> typing.Self: # -> "Motorcycle" : # -> Motorcycle? 91 | bike = Motorcycle(model, MotorcycleType.adventure, engine_size, off_road=True) 92 | return bike 93 | 94 | def __str__(self) -> str: 95 | return (f'{self.model} {self.engine_size}, type: {self.style}, ' + 96 | f'off-road: {self.off_road}, can jump: {self.can_jump}') 97 | 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /code/07-typing-guidance-patterns/minimalism.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | 4 | def main(): 5 | nums = [1, 1, 2, 3, 5] 6 | func(nums) 7 | 8 | nums2 = (1, 1, 2, 3, 5) 9 | func(nums2) 10 | 11 | nums3 = (n * n for n in nums) 12 | func(nums3) 13 | 14 | 15 | def func(things: Iterable[int]): 16 | print(type(things)) 17 | for t in things: 18 | print(f'{t}^2 = {t * t:,}', end=', ') 19 | print() 20 | 21 | 22 | def func_cluttered(things: list[int]): 23 | print(type(things)) 24 | for t in things: 25 | print(f'{t}^2 = {t * t:,}', end=', ') 26 | print() 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /code/07-typing-guidance-patterns/motorbike.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import typing 3 | 4 | 5 | class BikeType(enum.StrEnum): 6 | sport = "sport" 7 | naked = "naked" 8 | touring = "touring" 9 | adventure = "adventure" 10 | dual_sport = "dual_sport" 11 | motocross = "motocross" 12 | cross_country = "cross_country" 13 | trail = "trail" 14 | 15 | 16 | class MotorBike: 17 | 18 | def __init__(self, model: str, style: BikeType, engine_size: int, off_road: bool): 19 | self.model: str = model 20 | self.style: BikeType = style 21 | self.engine_size: int = engine_size 22 | self.off_road: bool = off_road 23 | 24 | @property 25 | def can_jump(self) -> bool: 26 | return self.off_road 27 | 28 | @classmethod 29 | def create_adventure(cls, model: str, engine_size: int) -> typing.Self: # -> Motorcycle? 30 | bike = MotorBike(model, BikeType.adventure, engine_size, off_road=True) 31 | return bike 32 | 33 | def __str__(self) -> str: 34 | return (f'{self.model} {self.engine_size}, type: {self.style}, ' + 35 | f'off-road: {self.off_road}, can jump: {self.can_jump}') 36 | -------------------------------------------------------------------------------- /code/07-typing-guidance-patterns/point_of_no_return.py: -------------------------------------------------------------------------------- 1 | from typing import NoReturn 2 | 3 | 4 | def main(): 5 | print("Fake web framework") 6 | 7 | try: 8 | text = input("Do you want to redirect or see the page? [y/n] ") 9 | if text == 'y': 10 | redirect_to_v2('https://talkpython.fm') 11 | # Never ran, warning!!! 12 | print("We are redirecting the user...") 13 | 14 | print("You see the page content and are amazed!") 15 | except HTTPException as x: 16 | print(f"Framework redirect exception: {x}") 17 | except ValueError: 18 | print("Framework: 400 bad request") 19 | 20 | 21 | # region Exceptions... 22 | class HTTPException(Exception): ... 23 | 24 | 25 | class HTTPFound(HTTPException): ... 26 | 27 | 28 | class HTTPMovedPermanently(HTTPException): ... 29 | 30 | 31 | # endregion 32 | 33 | # def func() -> None: 34 | # print('hi') 35 | 36 | def redirect_to_v2(url, permanent=False) -> NoReturn: # AlwaysException 37 | if not url: 38 | raise ValueError('url') 39 | 40 | if permanent: 41 | raise HTTPMovedPermanently(url) 42 | 43 | raise HTTPFound(url) 44 | 45 | 46 | def redirect_to_v1(url, permanent=False) -> None: 47 | if not url: 48 | raise ValueError('url') 49 | 50 | if permanent: 51 | raise HTTPMovedPermanently(url) 52 | 53 | raise HTTPFound(url) 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /code/07-typing-guidance-patterns/refactoring_types.py: -------------------------------------------------------------------------------- 1 | from motorbike import MotorBike 2 | 3 | 4 | def create_some_bikes() -> list[MotorBike]: 5 | motorcycles = [ 6 | MotorBike.create_adventure("Himalayan", 410), 7 | MotorBike.create_adventure("Tenere", 700), 8 | MotorBike.create_adventure("KTM", 790), 9 | MotorBike.create_adventure("Norden", 901), 10 | ] 11 | 12 | return motorcycles 13 | 14 | 15 | if __name__ == '__main__': 16 | 17 | print("Some bikes: ") 18 | bikes = create_some_bikes() 19 | for b in bikes: 20 | print(b) 21 | -------------------------------------------------------------------------------- /course_image/type-hints.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/rock-solid-python-with-type-hints-course/d96c478ff13c9f1ec2c58fe712c0b829409f9eb5/course_image/type-hints.webp -------------------------------------------------------------------------------- /requirements.piptools: -------------------------------------------------------------------------------- 1 | pydantic 2 | 3 | passlib 4 | argon2_cffi 5 | beanie 6 | fastapi 7 | jinja2 8 | uvicorn 9 | 10 | mypy 11 | beartype 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile requirements.piptools --output-file requirements.txt 3 | annotated-types==0.7.0 4 | # via pydantic 5 | anyio==4.4.0 6 | # via 7 | # httpx 8 | # starlette 9 | # watchfiles 10 | argon2-cffi==23.1.0 11 | # via -r requirements.piptools 12 | argon2-cffi-bindings==21.2.0 13 | # via argon2-cffi 14 | beanie==1.26.0 15 | # via -r requirements.piptools 16 | beartype==0.18.5 17 | # via -r requirements.piptools 18 | certifi==2024.7.4 19 | # via 20 | # httpcore 21 | # httpx 22 | cffi==1.16.0 23 | # via argon2-cffi-bindings 24 | click==8.1.7 25 | # via 26 | # beanie 27 | # typer 28 | # uvicorn 29 | dnspython==2.6.1 30 | # via 31 | # email-validator 32 | # pymongo 33 | email-validator==2.2.0 34 | # via fastapi 35 | fastapi==0.111.0 36 | # via -r requirements.piptools 37 | fastapi-cli==0.0.4 38 | # via fastapi 39 | h11==0.14.0 40 | # via 41 | # httpcore 42 | # uvicorn 43 | httpcore==1.0.5 44 | # via httpx 45 | httptools==0.6.1 46 | # via uvicorn 47 | httpx==0.27.0 48 | # via fastapi 49 | idna==3.7 50 | # via 51 | # anyio 52 | # email-validator 53 | # httpx 54 | jinja2==3.1.4 55 | # via 56 | # -r requirements.piptools 57 | # fastapi 58 | lazy-model==0.2.0 59 | # via beanie 60 | markdown-it-py==3.0.0 61 | # via rich 62 | markupsafe==2.1.5 63 | # via jinja2 64 | mdurl==0.1.2 65 | # via markdown-it-py 66 | motor==3.5.0 67 | # via beanie 68 | mypy==1.10.1 69 | # via -r requirements.piptools 70 | mypy-extensions==1.0.0 71 | # via mypy 72 | orjson==3.10.6 73 | # via fastapi 74 | passlib==1.7.4 75 | # via -r requirements.piptools 76 | pycparser==2.22 77 | # via cffi 78 | pydantic==2.8.2 79 | # via 80 | # -r requirements.piptools 81 | # beanie 82 | # fastapi 83 | # lazy-model 84 | pydantic-core==2.20.1 85 | # via pydantic 86 | pygments==2.18.0 87 | # via rich 88 | pymongo==4.8.0 89 | # via motor 90 | python-dotenv==1.0.1 91 | # via uvicorn 92 | python-multipart==0.0.9 93 | # via fastapi 94 | pyyaml==6.0.1 95 | # via uvicorn 96 | rich==13.7.1 97 | # via typer 98 | shellingham==1.5.4 99 | # via typer 100 | sniffio==1.3.1 101 | # via 102 | # anyio 103 | # httpx 104 | starlette==0.37.2 105 | # via fastapi 106 | toml==0.10.2 107 | # via beanie 108 | typer==0.12.3 109 | # via fastapi-cli 110 | typing-extensions==4.12.2 111 | # via 112 | # fastapi 113 | # mypy 114 | # pydantic 115 | # pydantic-core 116 | # typer 117 | ujson==5.10.0 118 | # via fastapi 119 | uvicorn==0.30.1 120 | # via 121 | # -r requirements.piptools 122 | # fastapi 123 | uvloop==0.19.0 124 | # via uvicorn 125 | watchfiles==0.22.0 126 | # via uvicorn 127 | websockets==12.0 128 | # via uvicorn 129 | --------------------------------------------------------------------------------