├── .gitignore ├── README.md ├── resources └── readme_images │ ├── shorts-poster-sm.png │ └── shorts-poster.jpg └── shorts ├── 01-merging-dictionaries ├── v1_non_pythonic_merge.py ├── v2_pythonic_py3_merge.py └── v3_pythonic_py310_merge.py ├── 02-parse-validate-with-pydantic ├── code │ ├── data.json │ ├── receive_data.py │ └── requirements.txt └── readme.md ├── 03-counting-occurrences-in-two-lines └── votes.py ├── 04-loop-to-comprehension ├── MOCK_DATA.json └── loop-to-comprehension.py ├── 05-beyond-the-list-comprehension ├── MOCK_DATA.json └── beyond-the-list-comprehension.py ├── 06-timedeltas-and-division └── duration_app.py ├── 07-inside-python-311 ├── gh-90908-task-groups.py ├── pep-654-exception-groups-and-except-star.py ├── pep-673-self-type.py ├── pep-675-arbitrary-literal-string-type.py ├── pep-678-exceptions-can-be-enriched-with-notes.py └── speedy.py ├── 08-walrus-operator └── walrus-app.py └── 09-gc-settings ├── requirements.txt └── run_gc_configured.py /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/.gitignore 131 | .idea/misc.xml 132 | .idea/modules.xml 133 | .idea/python-shorts.iml 134 | .idea/vcs.xml 135 | .idea/inspectionProfiles/profiles_settings.xml 136 | .idea/inspectionProfiles/Project_Default.xml 137 | /prompt=./bin/activate 138 | /prompt=./bin/activate.csh 139 | /prompt=./bin/activate.fish 140 | /prompt=./bin/Activate.ps1 141 | /prompt=./bin/pip 142 | /prompt=./bin/pip3 143 | /prompt=./bin/pip3.10 144 | /prompt=./bin/pip3.11 145 | /prompt=./bin/python 146 | /prompt=./bin/python3 147 | /prompt=./bin/python3.11 148 | /prompt=./pyvenv.cfg 149 | /.idea/dataSources.xml 150 | /.idea/sqldialects.xml 151 | identifier.sqlite 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python shorts 2 | 3 | Learn Python tips, tools, and techniques in around 5 minutes each. 4 | 5 | ## Watch on YouTube 6 | 7 | [![](./resources/readme_images/shorts-poster-sm.png)](https://talkpython.fm/python-shorts) 8 | 9 | [Subscribe on YouTube](https://talkpython.fm/python-shorts) to keep up with all the videos. 10 | 11 | ## Published videos 12 | 13 | - [scheduled Jan 28, 2022] **Combining dictionaries, the Python 3.10 way** 14 | - [scheduled Jan 25, 2022] **Beyond the List Comprehension** 15 | - [scheduled Jan 22, 2022] **for-in Loops to List Comprehensions** 16 | - **[Counting the number of times items appear with collections.Counter](https://youtu.be/0IHAm8cwlbQ)** 17 | - **[Parsing data with Pydantic](https://youtu.be/aHv7-6WIxNM)** 18 | -------------------------------------------------------------------------------- /resources/readme_images/shorts-poster-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/python-shorts/eb598f6382f697d43c3bf38e306ae8c04d1a6e01/resources/readme_images/shorts-poster-sm.png -------------------------------------------------------------------------------- /resources/readme_images/shorts-poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/python-shorts/eb598f6382f697d43c3bf38e306ae8c04d1a6e01/resources/readme_images/shorts-poster.jpg -------------------------------------------------------------------------------- /shorts/01-merging-dictionaries/v1_non_pythonic_merge.py: -------------------------------------------------------------------------------- 1 | query = {'id': 1, 'render_fast': True} 2 | post = {'email': 'j@j.com', 'name': 'Jeff'} 3 | route = {'id': 271, 'title': 'Fast apps'} 4 | 5 | # Non-pythonic procedural way 6 | merged = {} 7 | for k in query: 8 | merged[k] = query[k] 9 | for k in post: 10 | merged[k] = post[k] 11 | for k in route: 12 | merged[k] = route[k] 13 | 14 | 15 | print() 16 | print(f'{query=}') 17 | print(f'{post=}') 18 | print(f'{route=}') 19 | print() 20 | print('Merged via looping') 21 | print(f'{merged=}') 22 | -------------------------------------------------------------------------------- /shorts/01-merging-dictionaries/v2_pythonic_py3_merge.py: -------------------------------------------------------------------------------- 1 | 2 | query = {'id': 1, 'render_fast': True} 3 | post = {'email': 'j@j.com', 'name': 'Jeff'} 4 | route = {'id': 271, 'title': 'Fast apps'} 5 | 6 | # Python 3 Pythonic procedural way 7 | merged = {**query, **post, **route} 8 | 9 | 10 | print() 11 | print(f'{query=}') 12 | print(f'{post=}') 13 | print(f'{route=}') 14 | print() 15 | print('merged = {**query, **post, **route}') 16 | print(f'{merged=}') 17 | -------------------------------------------------------------------------------- /shorts/01-merging-dictionaries/v3_pythonic_py310_merge.py: -------------------------------------------------------------------------------- 1 | 2 | query = {'id': 1, 'render_fast': True} 3 | post = {'email': 'j@j.com', 'name': 'Jeff'} 4 | route = {'id': 271, 'title': 'Fast apps'} 5 | 6 | # Python 3.10 Pythonic procedural way 7 | merged = query | post | route 8 | 9 | print() 10 | print(f'{query=}') 11 | print(f'{post=}') 12 | print(f'{route=}') 13 | print() 14 | print('merged = query | post | route') 15 | print(f'{merged=}') 16 | -------------------------------------------------------------------------------- /shorts/02-parse-validate-with-pydantic/code/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Michael Kennedy", 3 | "age": "28", 4 | "location": { 5 | "city": "Portland", 6 | "state": "Oregon" 7 | }, 8 | "bike": "KTM Duke 690", 9 | "rides": [7, 103, 22, "70", 1000] 10 | } 11 | -------------------------------------------------------------------------------- /shorts/02-parse-validate-with-pydantic/code/receive_data.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | data = { 6 | "name": "Michael Kennedy", 7 | "age": "28", 8 | "location": { 9 | "city": "Portland", 10 | "state": "Oregon" 11 | }, 12 | "bike": "KTM Duke 690", 13 | "rides": [7, 103, 22, "70", 1000] 14 | } 15 | 16 | 17 | class Location(BaseModel): 18 | city: str 19 | state: str 20 | country: Optional[str] 21 | 22 | 23 | class User(BaseModel): 24 | name: str 25 | age: int 26 | location: Location 27 | bike: str 28 | rides: list[int] = [] 29 | 30 | 31 | user = User(**data) 32 | 33 | print(f"Found a user: {user}") 34 | -------------------------------------------------------------------------------- /shorts/02-parse-validate-with-pydantic/code/requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic 2 | -------------------------------------------------------------------------------- /shorts/02-parse-validate-with-pydantic/readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Parsing data with Pydantic 3 | 4 | ## _A Python short by Michael Kennedy_ 5 | 6 | **[Watch on YouTube](https://www.youtube.com/watch?v=aHv7-6WIxNM)** 7 | 8 | [![Watch on YouTube](https://img.youtube.com/vi/aHv7-6WIxNM/maxresdefault.jpg)](https://www.youtube.com/watch?v=aHv7-6WIxNM) 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /shorts/03-counting-occurrences-in-two-lines/votes.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | 4 | votes = [ 5 | 'other', 6 | 'Lewis Hamilton', 7 | 'Max Verstappen', 8 | 'Max Verstappen', 9 | 'other', 10 | 'other', 11 | 'Lewis Hamilton', 12 | 'other', 13 | 'Max Verstappen', 14 | 'Lewis Hamilton', 15 | 'Roman Grosjean', 16 | 'Roman Grosjean', 17 | 'Lewis Hamilton', 18 | 'Lewis Hamilton', 19 | 'Max Verstappen', 20 | 'other', 21 | 'other', 22 | 'other', 23 | 'Max Verstappen', 24 | 'Lewis Hamilton', 25 | 'other', 26 | 'other', 27 | 'Max Verstappen', 28 | 'Lewis Hamilton', 29 | 'Lewis Hamilton', 30 | 'Roman Grosjean', 31 | 'Lewis Hamilton', 32 | 'Max Verstappen', 33 | 'Lewis Hamilton', 34 | 'Lewis Hamilton', 35 | 'Lewis Hamilton', 36 | 'Lewis Hamilton', 37 | 'other', 38 | 'Max Verstappen', 39 | 'Lewis Hamilton', 40 | 'other', 41 | 'Roman Grosjean', 42 | 'Max Verstappen', 43 | 'Max Verstappen', 44 | 'Roman Grosjean', 45 | 'Lewis Hamilton', 46 | 'Max Verstappen', 47 | 'Lewis Hamilton', 48 | 'other', 49 | 'Lewis Hamilton', 50 | 'Lewis Hamilton', 51 | 'Roman Grosjean', 52 | 'Roman Grosjean', 53 | 'Lewis Hamilton', 54 | 'other', 55 | 'Max Verstappen', 56 | 'Max Verstappen', 57 | 'Lewis Hamilton', 58 | 'Lewis Hamilton', 59 | 'Roman Grosjean', 60 | 'other', 61 | 'other', 62 | 'Lewis Hamilton', 63 | 'other', 64 | 'Max Verstappen', 65 | 'Max Verstappen', 66 | 'Roman Grosjean', 67 | 'Lewis Hamilton', 68 | 'Lewis Hamilton', 69 | 'other', 70 | 'Roman Grosjean', 71 | 'Roman Grosjean', 72 | 'Lewis Hamilton', 73 | 'Max Verstappen', 74 | 'Lewis Hamilton', 75 | 'Roman Grosjean', 76 | 'other', 77 | 'other', 78 | 'other', 79 | 'Max Verstappen', 80 | 'other', 81 | 'Roman Grosjean', 82 | 'Lewis Hamilton', 83 | 'Max Verstappen', 84 | 'Max Verstappen', 85 | 'Max Verstappen', 86 | 'Lewis Hamilton', 87 | 'Max Verstappen', 88 | 'Lewis Hamilton', 89 | 'other', 90 | 'Roman Grosjean', 91 | 'Roman Grosjean', 92 | 'other', 93 | 'Max Verstappen', 94 | 'Max Verstappen', 95 | 'Lewis Hamilton', 96 | 'Roman Grosjean', 97 | 'Lewis Hamilton', 98 | 'other', 99 | 'Max Verstappen', 100 | 'Lewis Hamilton', 101 | 'Roman Grosjean', 102 | 'other', 103 | 'Lewis Hamilton', 104 | 'Lewis Hamilton', 105 | 'Lewis Hamilton', 106 | 'Lewis Hamilton', 107 | 'Lewis Hamilton', 108 | 'Lewis Hamilton', 109 | 'Lewis Hamilton', 110 | 'other', 111 | 'Lewis Hamilton', 112 | 'Lewis Hamilton', 113 | 'Max Verstappen', 114 | 'Lewis Hamilton', 115 | 'Max Verstappen', 116 | 'Lewis Hamilton', 117 | 'Lewis Hamilton', 118 | 'Lewis Hamilton', 119 | 'other', 120 | 'Max Verstappen', 121 | 'Lewis Hamilton', 122 | 'Lewis Hamilton', 123 | 'other', 124 | 'Lewis Hamilton', 125 | 'Lewis Hamilton', 126 | 'Lewis Hamilton', 127 | 'Max Verstappen', 128 | 'other', 129 | 'Lewis Hamilton', 130 | 'other', 131 | 'other', 132 | 'other', 133 | 'other', 134 | 'Roman Grosjean', 135 | 'Lewis Hamilton', 136 | 'Max Verstappen', 137 | 'Max Verstappen', 138 | 'Max Verstappen', 139 | 'Lewis Hamilton', 140 | 'Max Verstappen', 141 | 'Roman Grosjean', 142 | 'other', 143 | 'Lewis Hamilton', 144 | 'Roman Grosjean', 145 | 'Lewis Hamilton', 146 | 'Max Verstappen', 147 | 'other', 148 | 'other', 149 | 'other', 150 | 'other', 151 | 'Roman Grosjean', 152 | 'Max Verstappen', 153 | 'Max Verstappen', 154 | 'other', 155 | 'Roman Grosjean', 156 | 'other', 157 | 'Lewis Hamilton', 158 | 'Max Verstappen', 159 | 'Lewis Hamilton', 160 | 'other', 161 | 'Lewis Hamilton', 162 | 'Lewis Hamilton', 163 | 'Roman Grosjean', 164 | 'other', 165 | 'other', 166 | 'Lewis Hamilton', 167 | 'Max Verstappen', 168 | 'Lewis Hamilton', 169 | 'Lewis Hamilton', 170 | 'Max Verstappen', 171 | 'Max Verstappen', 172 | 'Lewis Hamilton', 173 | 'Lewis Hamilton', 174 | 'Max Verstappen', 175 | 'Lewis Hamilton', 176 | 'Roman Grosjean', 177 | 'Roman Grosjean', 178 | 'other', 179 | 'Roman Grosjean', 180 | 'Roman Grosjean', 181 | 'Lewis Hamilton', 182 | 'Lewis Hamilton', 183 | 'Lewis Hamilton', 184 | 'Lewis Hamilton', 185 | 'Lewis Hamilton', 186 | 'Lewis Hamilton', 187 | 'other', 188 | 'other', 189 | 'Roman Grosjean', 190 | 'Lewis Hamilton', 191 | 'other', 192 | 'other', 193 | 'other', 194 | 'Lewis Hamilton', 195 | 'Roman Grosjean', 196 | 'Lewis Hamilton', 197 | 'Max Verstappen', 198 | 'other', 199 | 'Lewis Hamilton', 200 | 'other', 201 | 'Lewis Hamilton', 202 | 'other', 203 | 'Max Verstappen', 204 | 'Lewis Hamilton', 205 | 'other', 206 | 'Max Verstappen', 207 | 'other', 208 | 'Lewis Hamilton', 209 | 'other', 210 | 'Lewis Hamilton', 211 | 'Roman Grosjean', 212 | 'Max Verstappen', 213 | 'other', 214 | 'other', 215 | 'Lewis Hamilton', 216 | 'other', 217 | 'other', 218 | 'Roman Grosjean', 219 | 'other', 220 | 'Roman Grosjean', 221 | 'other', 222 | 'other', 223 | 'Roman Grosjean', 224 | 'other', 225 | 'Lewis Hamilton', 226 | 'Roman Grosjean', 227 | 'Lewis Hamilton', 228 | 'Roman Grosjean', 229 | 'Max Verstappen', 230 | 'Lewis Hamilton', 231 | 'Lewis Hamilton', 232 | 'Lewis Hamilton', 233 | 'other', 234 | 'Lewis Hamilton', 235 | 'Roman Grosjean', 236 | 'Roman Grosjean', 237 | 'Lewis Hamilton', 238 | 'Max Verstappen', 239 | 'Lewis Hamilton', 240 | 'Lewis Hamilton', 241 | 'Max Verstappen', 242 | 'Max Verstappen', 243 | 'Lewis Hamilton', 244 | 'Max Verstappen', 245 | 'Lewis Hamilton', 246 | 'Roman Grosjean', 247 | 'Lewis Hamilton', 248 | 'Roman Grosjean', 249 | 'other', 250 | 'Max Verstappen', 251 | 'Roman Grosjean', 252 | 'Lewis Hamilton', 253 | 'Lewis Hamilton', 254 | 'other', 255 | 'other', 256 | 'Roman Grosjean', 257 | 'Max Verstappen', 258 | 'other', 259 | 'Lewis Hamilton', 260 | 'Lewis Hamilton', 261 | 'Roman Grosjean', 262 | 'Roman Grosjean', 263 | 'Lewis Hamilton', 264 | 'Lewis Hamilton', 265 | 'Lewis Hamilton', 266 | 'Roman Grosjean', 267 | 'Max Verstappen', 268 | 'Lewis Hamilton', 269 | 'Max Verstappen', 270 | 'other', 271 | 'other', 272 | 'Roman Grosjean', 273 | 'Roman Grosjean', 274 | 'Roman Grosjean', 275 | 'Roman Grosjean', 276 | ] 277 | 278 | 279 | c = Counter(votes) 280 | 281 | for name, count in c.most_common(): 282 | if name == "other": 283 | continue 284 | 285 | percent = int(count / c.total() * 100) 286 | print(f'{name}: {percent}%') 287 | -------------------------------------------------------------------------------- /shorts/04-loop-to-comprehension/loop-to-comprehension.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Iterator 4 | 5 | 6 | def main(): 7 | sales = load_list() 8 | 9 | count = 10 10 | for idx, s in enumerate(sales, start=1): 11 | if idx > count: 12 | break 13 | 14 | print(f'{idx}. {s.first_name} {s.last_name} bought {s.make} {s.model} for ${s.price:,}.') 15 | 16 | 17 | class AutoCustomer: 18 | id: int 19 | first_name: str 20 | last_name: str 21 | email: str 22 | make: str 23 | model: str 24 | year: int 25 | vin: str 26 | price: int 27 | 28 | def __init__(self, _id: int, first_name, last_name, email, make, model, year, vin, price): 29 | # print(f"Creating customer {_id}: {first_name} {last_name}") 30 | self.id = _id 31 | self.first_name = first_name 32 | self.last_name = last_name 33 | self.email = email 34 | self.make = make 35 | self.model = model 36 | self.year = year 37 | self.vin = vin 38 | self.price = price 39 | 40 | 41 | def load_list() -> list[AutoCustomer]: 42 | raw_data = get_raw_data() 43 | 44 | # *********** for-in loop *********** 45 | # customers = [] 46 | # for entry in raw_data: 47 | # c = AutoCustomer(**entry) 48 | # customers.append(c) 49 | 50 | # *********** List comprehension *********** 51 | return [AutoCustomer(**entry) for entry in raw_data] 52 | 53 | 54 | def get_raw_data(): 55 | file = Path(__file__).parent / 'MOCK_DATA.json' 56 | with file.open('r') as fin: 57 | # Use json-stream package to stream this too. 58 | # We're just keeping it simple 59 | raw_data = json.load(fin) 60 | return raw_data 61 | 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /shorts/05-beyond-the-list-comprehension/beyond-the-list-comprehension.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | 5 | def main(): 6 | sales: list[AutoCustomer] = load_list() 7 | 8 | # List comprehension 9 | # [FINAL_ITEM for ITEM in DATA if TEST] 10 | 11 | # Dictionary comprehension 12 | # {KEY:FINAL_ITEM for ITEM in DATA if TEST} 13 | 14 | # Set comprehension 15 | # {FINAL_ITEM for ITEM in DATA if TEST} 16 | 17 | by_email: dict[str, AutoCustomer] = {s.email: s for s in sales} 18 | by_vin: dict[str, AutoCustomer] = {s.vin: s for s in sales} 19 | unique_vins: set[str] = {s.vin for s in sales} 20 | 21 | email = input("Do a search by email: ") 22 | sale = by_email.get(email) 23 | print(sale) 24 | 25 | print() 26 | vin = input("Do a search by vin: ") 27 | sale = by_vin.get(vin) 28 | print(sale) 29 | 30 | print() 31 | input("Enter to continue...") 32 | print(unique_vins) 33 | print(f'Number of unique vins: {len(unique_vins)} out of {len(sales):,} sales') 34 | 35 | 36 | class AutoCustomer: 37 | id: int 38 | first_name: str 39 | last_name: str 40 | email: str 41 | make: str 42 | model: str 43 | year: int 44 | vin: str 45 | price: int 46 | 47 | def __init__(self, _id: int, first_name, last_name, email, make, model, year, vin, price): 48 | self.id = _id 49 | self.first_name = first_name 50 | self.last_name = last_name 51 | self.email = email 52 | self.make = make 53 | self.model = model 54 | self.year = year 55 | self.vin = vin 56 | self.price = price 57 | 58 | def __repr__(self): 59 | return f'<{self.email} bought {self.make} {self.model} ({self.vin}) for ${self.price:,}>' 60 | 61 | def __str__(self): 62 | return repr(self) 63 | 64 | 65 | def load_list() -> list[AutoCustomer]: 66 | return [AutoCustomer(**entry) for entry in (load_data())] 67 | 68 | 69 | def load_data(): 70 | file = Path(__file__).parent / 'MOCK_DATA.json' 71 | with file.open('r') as fin: 72 | # Use json-stream package to stream this too. 73 | # We're just keeping it simple 74 | raw_data = json.load(fin) 75 | return raw_data 76 | 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /shorts/06-timedeltas-and-division/duration_app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def main(): 5 | go_time = get_event_time('performance') 6 | start_time = get_event_time('arrival') 7 | 8 | dt: datetime.timedelta = go_time - start_time 9 | 10 | print(f"We're on stage in {dt.total_seconds()} seconds.") 11 | 12 | days = int(dt / datetime.timedelta(days=1)) % 7 13 | weeks = int(dt / datetime.timedelta(days=7)) 14 | print(f"We're on stage in {weeks} weeks and {days} day.") 15 | 16 | 17 | def get_event_time(name: str) -> datetime.datetime: 18 | if name == 'arrival': 19 | return datetime.datetime(year=2022, month=2, day=1, hour=8, minute=31) 20 | elif name == 'performance': 21 | return datetime.datetime(year=2022, month=2, day=16, hour=20, minute=00) 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/gh-90908-task-groups.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uuid 3 | 4 | 5 | async def main(): 6 | # gh-90908 introduces Task Groups for Python 3.11 7 | # All tasks created within the group tg will complete or error 8 | # when the with block is done. 9 | async with asyncio.TaskGroup() as tg: 10 | tg.create_task(save_log_message("This is an async msg")) 11 | conf_task = tg.create_task(get_confirmation()) 12 | 13 | print("Waiting for tasks to complete...", flush=True) 14 | 15 | print("All tasks have completed now.") 16 | 17 | conf: str = conf_task.result() # task is done so we have its result. 18 | print(f"Confirmation is {conf}") 19 | 20 | 21 | async def save_log_message(text: str): 22 | await asyncio.sleep(1.5) 23 | print(f"Saved log message: {text}") 24 | await asyncio.sleep(.5) 25 | 26 | 27 | async def get_confirmation() -> str: 28 | await asyncio.sleep(.5) 29 | conf = str(uuid.uuid4()) 30 | print("Confirmation ready.") 31 | return conf 32 | 33 | 34 | if __name__ == '__main__': 35 | asyncio.run(main()) 36 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/pep-654-exception-groups-and-except-star.py: -------------------------------------------------------------------------------- 1 | # PEP 654: Exception Groups and except* 2 | from tarfile import StreamError 3 | 4 | # PEP 654 introduces language features that enable a program to 5 | # raise and handle multiple unrelated exceptions simultaneously. 6 | # The builtin types ExceptionGroup and BaseExceptionGroup make it 7 | # possible to group exceptions and raise them together, and the new 8 | # except* syntax generalizes except to match subgroups of exception groups. 9 | 10 | def crazy_async_function(): 11 | raise ExceptionGroup( 12 | "Something bad", 13 | [ 14 | ValueError("Cannot read abc as int"), 15 | FileNotFoundError(), 16 | StreamError() 17 | ] 18 | ) 19 | 20 | try: 21 | crazy_async_function() 22 | except* ValueError: 23 | print("We've hit a ValueError!") 24 | except* KeyError as e: 25 | print("We've hit a KeyError!") 26 | except* (FileNotFoundError, StreamError) as e: 27 | e: ExceptionGroup 28 | for ex in e.exceptions: 29 | print(f"Another error, something with files: {type(ex).__name__}") 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/pep-673-self-type.py: -------------------------------------------------------------------------------- 1 | # PEP 673 – Self Type 2 | from typing import Self 3 | 4 | 5 | class Character: 6 | def __init__(self, name: str, level: 1): 7 | self.name = name 8 | self.level = level 9 | 10 | @classmethod 11 | def create(cls, name: str) -> Self: 12 | return cls(name, 1) 13 | 14 | 15 | class Wizard(Character): 16 | def cast_spell(self, spell_name: str): 17 | print(f"{self.name} of level {self.level} casts {spell_name}!") 18 | 19 | 20 | npc_cat = Character.create("Black Cat") 21 | gandolf = Wizard.create("Gandolf") 22 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/pep-675-arbitrary-literal-string-type.py: -------------------------------------------------------------------------------- 1 | # PEP 675 – Arbitrary Literal String Type 2 | from sqlite3 import Connection 3 | 4 | 5 | class User: ... 6 | 7 | 8 | conn = Connection() 9 | 10 | 11 | def query_user(connection: Connection, user_id: str) -> User: 12 | query = f"SELECT * FROM data WHERE user_id = {user_id}" 13 | connection.execute(query) 14 | return User() # With details from query 15 | 16 | 17 | query_user(conn, "user123") # OK. 18 | query_user(conn, "user123; DROP TABLE data;") # Delete the table. 19 | query_user(conn, "user123 OR 1 = 1") # Fetch all users (since 1 = 1 is always true). 20 | 21 | 22 | def query_user(connection: Connection, user_id: str) -> User: 23 | query = "SELECT * FROM data WHERE user_id = ?" # <-- Safe, literal string. 24 | connection.execute(query, (user_id,)) # <-- Safe, database parameter. 25 | return User() # With details from query 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | # def safer_func(text: str): 47 | # print(text) 48 | # x = 7 49 | # 50 | # literal_text: LiteralString = "ABC" 51 | # literal_text2 = f"ABC{x}" 52 | # safer_func(literal_text) 53 | # safer_func(literal_text2) 54 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/pep-678-exceptions-can-be-enriched-with-notes.py: -------------------------------------------------------------------------------- 1 | # PEP 678: Exceptions can be enriched with notes 2 | 3 | # The add_note() method is added to BaseException. It can be used to enrich 4 | # exceptions with context information that is not available at the time when 5 | # the exception is raised. The added notes appear in the default traceback. 6 | 7 | def some_system_func(val): 8 | # noinspection PyUnusedLocal 9 | x = val * 2 10 | raise ValueError("Cannot convert to text") 11 | 12 | 13 | # noinspection PyUnresolvedReferences 14 | def our_function(val): 15 | try: 16 | results = some_system_func(val) 17 | return results > 0 18 | except ValueError as ve: 19 | ve.add_note(f"This didn't work for value '{val}'") 20 | raise 21 | 22 | 23 | if __name__ == '__main__': 24 | our_function("abc") 25 | -------------------------------------------------------------------------------- /shorts/07-inside-python-311/speedy.py: -------------------------------------------------------------------------------- 1 | # Python 3.11 is 10-60% faster than 3.10 2 | import datetime 3 | import random 4 | import sys 5 | from typing import Tuple 6 | 7 | 8 | def find_some_distances(points): 9 | t0 = datetime.datetime.now() 10 | 11 | for (x1, y1), (x2, y2) in points: 12 | distance = ((x1 - x2) ** 2.0 + (y1 - y2) ** 2.0) ** 0.5 13 | 14 | 15 | dt = datetime.datetime.now() - t0 16 | # print(f"Computed distance for {len(points):,} points in {dt.total_seconds()*1000:,.0f} ms on " 17 | # f"Python {sys.version_info.major}:{sys.version_info.minor}") 18 | return dt.total_seconds() 19 | 20 | 21 | if __name__ == '__main__': 22 | warm_up_data = [(1,1),(2,2), (3,3)] 23 | warmup = list(zip(warm_up_data[:-1], warm_up_data[1:])) 24 | find_some_distances(warmup) 25 | 26 | larger_data = [ 27 | (random.random(),random.random(),) 28 | for _ in range(1, 1_000_000+1) 29 | ] 30 | larger = list(zip(larger_data[:-1], larger_data[1:])) 31 | 32 | times = [] 33 | for _ in range(1, 100): 34 | times.append(find_some_distances(larger)) 35 | 36 | # ave_sec = sum(times)/len(times) 37 | ave_sec = min(times) 38 | 39 | print(f"Done in min {ave_sec*1000:,.1f} ms for {sys.version_info.major}:{sys.version_info.minor}") 40 | -------------------------------------------------------------------------------- /shorts/08-walrus-operator/walrus-app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | from typing import Optional 4 | 5 | # PEP 572 – Assignment Expressions (aka walrus operator) 6 | 7 | # What is it? 8 | creator = "Professor Falken" 9 | 10 | name = input("What is your name? ") 11 | if name == creator: 12 | print(f"Greetings {name}, would you like to play a game?") 13 | 14 | # Using the walrus: 15 | # Mistake in the video: Forgot to add the () here and name = True rather than input. 16 | if (name := input("What is your name? ")) == creator: 17 | print(f"Greetings {name}, would you like to play a game?") 18 | 19 | # **************************************************************************** 20 | r = re.compile(pattern=".* ([0-9]+)x([0-9]+) .*") 21 | text = 'This video has a resolution of 2560x1440 pixels.' 22 | 23 | match = r.match(text) 24 | if match and len(match.groups()) >= 2: 25 | w = match.groups()[0] 26 | h = match.groups()[1] 27 | print(f'The new resolution is width: {w} x {h}') 28 | 29 | # Using the walrus: 30 | 31 | 32 | if match := r.match(text) and len(g := match.groups()) >= 2: 33 | print(f'The new resolution is width: {g[0]} x {g[1]}') 34 | 35 | 36 | # **************************************************************************** 37 | class User: 38 | id_: int 39 | created_date: datetime.datetime 40 | 41 | def __init__(self, id_: int, date): 42 | self.id_ = id_ 43 | self.created_date = date 44 | 45 | def __str__(self): 46 | return f'User: id={self.id_}, created={self.created_date.date().isoformat()}' 47 | 48 | def __repr__(self): 49 | return str(self) 50 | 51 | 52 | def get_user_by_id(user_id: int) -> Optional[User]: 53 | now = datetime.datetime.now() 54 | rand_users = dict() 55 | rand_users[2] = User(2, now - datetime.timedelta(days=1)) 56 | rand_users[7] = User(7, now - datetime.timedelta(days=100)) 57 | rand_users[11] = User(11, now - datetime.timedelta(days=2)) 58 | 59 | return rand_users.get(user_id) 60 | 61 | 62 | user_ids = [ 63 | 1, 2, 7, 11, ] 64 | cutoff_date = datetime.datetime.now() - datetime.timedelta(days=7) 65 | 66 | active_users = [ 67 | get_user_by_id(uid) 68 | for uid in user_ids 69 | if get_user_by_id(uid) and get_user_by_id(uid).created_date > cutoff_date 70 | ] 71 | 72 | print(active_users) 73 | 74 | # Using the walrus: 75 | active_users = [ 76 | user 77 | for uid in user_ids 78 | if (user := get_user_by_id(uid)) and user.created_date > cutoff_date 79 | ] 80 | 81 | print(active_users) 82 | 83 | # **************************************************************************** 84 | 85 | prompt_text = "Which action? [a], [b], or [c] (ENTER to exit)? " 86 | command = input(prompt_text) 87 | 88 | while command.strip() != '': 89 | print(f"You chose to perform action '{command}'!") 90 | command = input(prompt_text) 91 | 92 | print("Bye now!") 93 | 94 | # Using the walrus: 95 | 96 | 97 | while (command := input(prompt_text)).strip() != '': 98 | print(f"You chose to perform action '{command}'!") 99 | 100 | print("Bye now!") 101 | 102 | # **************************************************************************** 103 | # What value matched any() or the all()? 104 | values = [55, 7, 22, 200, 15, -5, 75, -20, -10] 105 | if any(v < 0 for v in values): 106 | print("We have negatives!") 107 | else: 108 | print("Only non-negative numbers for us.") 109 | 110 | if all(v < 0 for v in values): 111 | print("ALL numbers are negatives!") 112 | else: 113 | print("At least one non-negative number exists.") 114 | 115 | # Using the walrus: 116 | if any((first := v) < 0 for v in values): 117 | print(f"We have negatives, the first is {first}!") 118 | else: 119 | print("Only non-negative numbers for us.") 120 | 121 | if all((counterpoint := v) < 0 for v in values): 122 | print("ALL numbers are negatives!") 123 | else: 124 | print(f"At least one non-negative number exists, for example: {counterpoint}") 125 | -------------------------------------------------------------------------------- /shorts/09-gc-settings/requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | colorama 3 | -------------------------------------------------------------------------------- /shorts/09-gc-settings/run_gc_configured.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import gc 3 | import os 4 | import random 5 | 6 | import psutil 7 | from colorama import Fore 8 | 9 | 10 | def main(change_gc: bool): 11 | count = 1000 12 | times = 100 13 | 14 | m0 = report_process_mem() 15 | 16 | # gc.set_debug(gc.DEBUG_STATS) 17 | # Moved gc.freeze() to start up code. Didn't belong here. 18 | 19 | if change_gc: 20 | allocs, g1, g2 = gc.get_threshold() 21 | gc.set_threshold(100_000, g1*5, g2*10) 22 | 23 | lists_of_items = [] 24 | for items in range(0, times): 25 | lists_of_items.append(create_some_items(count)) 26 | 27 | m1 = report_process_mem() 28 | 29 | return m1 - m0 30 | 31 | 32 | def report_process_mem() -> int: 33 | process = psutil.Process(os.getpid()) 34 | mb = process.memory_info().rss / 1024 / 1024 35 | print(f"Total memory used: {mb:,.2f} MB.") 36 | 37 | return mb 38 | 39 | 40 | class DbItem: 41 | def __init__(self, x: float, y: list[dict]): 42 | self.x = x 43 | self.y = y 44 | 45 | 46 | def create_some_items(count: int) -> list[DbItem]: 47 | items = [] 48 | 49 | for _ in range(0, count): 50 | lst = [{} for _ in range(0, 20)] 51 | item = DbItem(random.random(), lst) 52 | items.append(item) 53 | 54 | return items 55 | 56 | 57 | if __name__ == '__main__': 58 | random.seed(12345) # Make it repeatable 59 | create_some_items(1) # Warm up the code so we don't time startup 60 | 61 | change = input("Use modified GC settings? [y]/n ") in {'', 'y'} 62 | 63 | # Moved this to the start up of the app (from main() in video). 64 | # We shouldn't be timing long term speed with this included. 65 | gc.collect(2) 66 | gc.freeze() 67 | 68 | t0 = datetime.datetime.now() 69 | 70 | # Let's go! 71 | mem_delta = main(change) 72 | 73 | dt = datetime.datetime.now() - t0 74 | msg = (Fore.LIGHTRED_EX + 'with') if change else (Fore.CYAN + 'without') 75 | print(f"Done {msg + Fore.RESET} modifying the GC.") 76 | print(f'Memory used: {mem_delta:,.0f} MB') 77 | print(f'Time:{Fore.LIGHTYELLOW_EX} {dt.total_seconds() * 1000:,.1f} ms') 78 | --------------------------------------------------------------------------------