<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Trey Hunner - Python</title><link href="https://treyhunner.com/" rel="alternate"/><link href="https://treyhunner.com/python.xml" rel="self"/><id>https://treyhunner.com/</id><updated>2026-09-03T17:30:00-07:00</updated><subtitle>I help developers level-up their Python skills</subtitle><entry><title>Python Morsels now has spaced repetition</title><link href="https://treyhunner.com/2026/09/python-morsels-now-has-spaced-repetition/" rel="alternate"/><published>2026-09-03T17:30:00-07:00</published><updated>2026-09-03T17:30:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2026/09/python-morsels-now-has-spaced-repetition</id><content type="html">&lt;p&gt;Nearly every book I&amp;rsquo;ve read on teaching and education over the past decade has talked about the value of &lt;strong&gt;spaced repetition&lt;/strong&gt;.
For much of that time, spaced repetition has been something I would &lt;em&gt;recommend&lt;/em&gt; learners do, but it wasn&amp;rsquo;t something I &lt;em&gt;helped&lt;/em&gt; anyone do&amp;hellip; until now.&lt;/p&gt;
&lt;p&gt;Python Morsels now has &lt;a href="https://www.pythonmorsels.com/recall/"&gt;Daily Recall&lt;/a&gt;: a spaced repetition system &lt;strong&gt;for Python programmers&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;We learn by recalling, not by reading&lt;/h2&gt;
&lt;p&gt;The most effective learning techniques all rely on &lt;strong&gt;active recall&lt;/strong&gt;: trying to remember something &lt;em&gt;without&lt;/em&gt; looking it up, whether with flash cards, by explaining an idea in your own words, or by doing a task that requires it.&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t learn by putting information &lt;em&gt;into&lt;/em&gt; our heads.
We learn by retrieving information &lt;em&gt;from&lt;/em&gt; our heads.
That&amp;rsquo;s why Python Morsels has always been built around exercises rather than videos: writing code is the most useful form of recall for a Python programmer.&lt;/p&gt;
&lt;p&gt;But not everything worth remembering warrants an entire Python exercise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which &lt;code&gt;uv&lt;/code&gt; command runs a tool without installing it?&lt;/li&gt;
&lt;li&gt;What is the time complexity of various list operations?&lt;/li&gt;
&lt;li&gt;Which string method can remove a substring from the end of a string?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A 20-minute exercise is overkill for practicing something that small.
But practicing it just once isn&amp;rsquo;t enough either.
A quick question, asked again just before you&amp;rsquo;d forget, is a much better fit.&lt;/p&gt;
&lt;h2&gt;Spaced repetition beats the forgetting curve&lt;/h2&gt;
&lt;p&gt;Many of the things I learned in school are long gone, especially the ones I haven&amp;rsquo;t thought about even once in &lt;em&gt;years&lt;/em&gt;.
I think I could explain photosynthesis in 9th grade.
I can&amp;rsquo;t today.&lt;/p&gt;
&lt;p&gt;This is explained by &lt;a href="https://en.wikipedia.org/wiki/Forgetting_curve"&gt;the forgetting curve&lt;/a&gt;: we forget what we don&amp;rsquo;t recall, and the rate of forgetting is somewhat predictable.
&lt;strong&gt;Spaced repetition&lt;/strong&gt; is about using active recall to beat the forgetting curve.
Instead of recalling an idea over and over right after learning it, you wait until it has &lt;em&gt;started&lt;/em&gt; to fade, and then try to recall it.
Each successful recall earns a longer wait before the next one: minutes at first, then hours, then days, and eventually weeks and months.&lt;/p&gt;
&lt;p&gt;The tricky part is the timing: &lt;em&gt;when&lt;/em&gt; should you try to recall each thing?
That&amp;rsquo;s where an algorithm helps.
A spaced repetition system tracks every idea you&amp;rsquo;re trying to remember and prompts you to recall each one right before you&amp;rsquo;d forget it.&lt;/p&gt;
&lt;h2&gt;What Daily Recall does&lt;/h2&gt;
&lt;p&gt;Back in April, just before Earth Day, I made &lt;a href="https://whereabouts.earth"&gt;Whereabouts.Earth&lt;/a&gt; to help me learn the name and location of every country in the world.
When I started, I could name about 90 of the 197 countries on a map.
By mid-June, with about 10 minutes of practice a day, I knew all of them.&lt;/p&gt;
&lt;p&gt;Daily Recall is the same idea, but for Python.
You pick the packs you want to practice, and each day it asks you a few questions from them.
Answer a question correctly and it&amp;rsquo;ll be a while before you see that one again.
Miss it and you&amp;rsquo;ll see it again soon.&lt;/p&gt;
&lt;p&gt;Daily Recall uses &lt;a href="https://github.com/open-spaced-repetition/fsrs4anki/wiki/ABC-of-FSRS"&gt;FSRS&lt;/a&gt; for scheduling, which is one of the most effective spaced repetition algorithms (it&amp;rsquo;s an option in the &lt;a href="https://apps.ankiweb.net/"&gt;Anki&lt;/a&gt; flash card app).&lt;/p&gt;
&lt;p&gt;If you have trouble remembering which operations on different data structures are fast and which are slow, there&amp;rsquo;s a &lt;a href="https://www.pythonmorsels.com/recall/packs/time-complexity/"&gt;Time Complexity pack&lt;/a&gt; for that.
If you&amp;rsquo;re struggling to remember the &lt;em&gt;many&lt;/em&gt; different subcommands that &lt;code&gt;uv&lt;/code&gt; supports, there&amp;rsquo;s a &lt;a href="https://www.pythonmorsels.com/recall/packs/uv/"&gt;uv pack&lt;/a&gt; for that.
There are also &lt;a href="https://www.pythonmorsels.com/recall/packs/"&gt;packs&lt;/a&gt; on string methods, built-in functions, dictionaries, iterable unpacking, f-strings, pytest, and what&amp;rsquo;s new in Python 3.13 and 3.14.
I&amp;rsquo;m hoping to release about one new pack each week over the next many months.&lt;/p&gt;
&lt;p&gt;Daily Recall also &lt;strong&gt;works well on a phone&lt;/strong&gt; because recall questions don&amp;rsquo;t require typing a bunch of code.
So you can &lt;strong&gt;replace 5 minutes of your daily doomscrolling&lt;/strong&gt; with 5 minutes of extra Python learning.&lt;/p&gt;
&lt;p&gt;Early users who practiced about 5 minutes a day ended their first month with 25 to 50 new things they could still recall weeks after last seeing them.&lt;/p&gt;
&lt;h2&gt;You can use Daily Recall for free&lt;/h2&gt;
&lt;p&gt;Most &lt;a href="https://www.pythonmorsels.com/recall/"&gt;Daily Recall&lt;/a&gt; packs are free, and the rest are included with the &lt;a href="https://www.pythonmorsels.com/pricing/"&gt;All Access&lt;/a&gt; plan.
To get started, &lt;a href="https://www.pythonmorsels.com/accounts/signup/"&gt;create a free Python Morsels account&lt;/a&gt;, pick a pack or two, and answer a few questions.&lt;/p&gt;
&lt;p&gt;And whether or not you ever use Daily Recall, I&amp;rsquo;d recommend spaced repetition.
If you&amp;rsquo;d rather write your own flash cards, on Python or anything else, &lt;a href="https://apps.ankiweb.net/"&gt;Anki&lt;/a&gt; works well too.
The next time you learn something new in Python, don&amp;rsquo;t just read it a second time.
Try to recall it tomorrow, and then again next week.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.pythonmorsels.com/recall/" class="subscribe-btn form-big"&gt;Try Daily Recall&lt;/a&gt;&lt;/p&gt;</content><category term="python"/><category term="learning"/></entry><entry><title>Reorganizing Python's sys module</title><link href="https://treyhunner.com/2026/08/reorganizing-pythons-sys-module/" rel="alternate"/><published>2026-08-12T17:30:00-07:00</published><updated>2026-08-12T17:30:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2026/08/reorganizing-pythons-sys-module</id><content type="html">&lt;p&gt;The &lt;code&gt;sys&lt;/code&gt; module is &lt;strong&gt;the primary junk drawer&lt;/strong&gt; of the Python standard library.&lt;/p&gt;
&lt;p&gt;A good junk drawer holds miscellaneous items that don&amp;rsquo;t have another sensible home.&lt;/p&gt;
&lt;p&gt;I often think of &lt;code&gt;utils&lt;/code&gt; modules as &amp;ldquo;junk drawer&amp;rdquo; modules.
I believe that both &lt;code&gt;utils&lt;/code&gt; modules and junk drawers have their purpose, but junk drawers can get out of hand.&lt;/p&gt;
&lt;p&gt;As I recently noted in &lt;a href="https://treyhunner.com/pathlib-talk/"&gt;my talk about pathlib&lt;/a&gt;, I see both the &lt;code&gt;sys&lt;/code&gt; module and &lt;a href="https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/"&gt;the &lt;code&gt;os&lt;/code&gt; module&lt;/a&gt; as &lt;strong&gt;junk drawer modules&lt;/strong&gt;.
They serve a similar purpose to &lt;code&gt;utils&lt;/code&gt; modules, but they have a different name.&lt;/p&gt;
&lt;p&gt;In this post, I wonder: what would Python&amp;rsquo;s &lt;code&gt;sys&lt;/code&gt; be like if it was designed today, from scratch?&lt;/p&gt;
&lt;h2&gt;The overview: 9 new &lt;code&gt;sys&lt;/code&gt; submodules&lt;/h2&gt;
&lt;p&gt;As of Python 3.15, the &lt;code&gt;sys&lt;/code&gt; module has 115-121 attributes (depending on whether you&amp;rsquo;re in the REPL and whether an exception has occurred), and 63 of those are functions.&lt;/p&gt;
&lt;p&gt;All of those names need permanent homes.&lt;/p&gt;
&lt;p&gt;How can we reorganize a &lt;code&gt;utils&lt;/code&gt;-style module that&amp;rsquo;s grown &lt;em&gt;quite&lt;/em&gt; large?&lt;/p&gt;
&lt;p&gt;Give it submodules!&lt;/p&gt;
&lt;p&gt;No, really&amp;hellip; this isn&amp;rsquo;t the &lt;em&gt;worst&lt;/em&gt; solution.
&lt;a href="https://docs.djangoproject.com/en/stable/ref/utils/"&gt;Django&amp;rsquo;s &lt;code&gt;utils&lt;/code&gt; package&lt;/a&gt; isn&amp;rsquo;t so bad.&lt;/p&gt;
&lt;p&gt;So we could split &lt;code&gt;sys&lt;/code&gt; into these 9 submodules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sys.cli&lt;/code&gt;: for command-line argument handling and program control&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.imports&lt;/code&gt;: related to imports and modules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.io&lt;/code&gt;: handles standard I/O streams&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.repl&lt;/code&gt;: handles REPL display control&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.interpreter&lt;/code&gt;: system information and installation details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.memory&lt;/code&gt;: memory management tools used for profiling, optimization, and debugging&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.exceptions&lt;/code&gt;: exception handling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.profile&lt;/code&gt;: profiling and introspection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.runtime&lt;/code&gt;: interpreter runtime behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You may notice some similarities to other modules in the Python standard library.
That&amp;rsquo;s a hint that it &lt;em&gt;might&lt;/em&gt; be worth moving some of these utilities into &lt;em&gt;other parts&lt;/em&gt; of the standard library.
But moving functionality between top-level modules is a much bigger change, so let&amp;rsquo;s set that idea aside.&lt;/p&gt;
&lt;p&gt;For now, let&amp;rsquo;s take a closer look at our tentatively re-organized &lt;code&gt;sys&lt;/code&gt; package.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;sys&lt;/code&gt; package and its submodules&lt;/h2&gt;
&lt;p&gt;A couple of these 9 submodules have only a handful of attributes, a couple have over 20, and the rest fall somewhere in the middle.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;sys.cli&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This submodule would handle command-line arguments and program control:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;argv&lt;/code&gt;: Command-line arguments list&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orig_argv&lt;/code&gt;: Original unmodified arguments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exit()&lt;/code&gt;: Function to terminate Python&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;: Named tuple of interpreter flags&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_xoptions&lt;/code&gt;: Dictionary of &lt;code&gt;-X&lt;/code&gt; command options&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.imports&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Everything related to imports and modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt;: Module search path list&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modules&lt;/code&gt;: Dictionary of loaded modules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;builtin_module_names&lt;/code&gt;: Tuple of built-in modules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stdlib_module_names&lt;/code&gt;: Frozen set of standard library modules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path_hooks&lt;/code&gt;: List of path-to-finder callables&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path_importer_cache&lt;/code&gt;: Finder object cache&lt;/li&gt;
&lt;li&gt;&lt;code&gt;meta_path&lt;/code&gt;: Meta path finder objects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pycache_prefix&lt;/code&gt;: Bytecode cache directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_lazy_imports()&lt;/code&gt;, &lt;code&gt;get_lazy_imports()&lt;/code&gt;, &lt;code&gt;set_lazy_imports_filter()&lt;/code&gt;, &lt;code&gt;get_lazy_imports_filter()&lt;/code&gt;, &lt;code&gt;lazy_modules&lt;/code&gt;: Lazy import controls (Python 3.15+)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.io&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The standard I/O streams:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stdin&lt;/code&gt;: Standard input stream&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stdout&lt;/code&gt;: Standard output stream&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stderr&lt;/code&gt;: Standard error stream&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__stdin__&lt;/code&gt;, &lt;code&gt;__stdout__&lt;/code&gt;, &lt;code&gt;__stderr__&lt;/code&gt;: The original values of those three streams (useful for restoring them after replacing them)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.repl&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Hooks and settings for Python&amp;rsquo;s interactive prompt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;displayhook()&lt;/code&gt;: Called to show the result of each REPL expression&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__displayhook__&lt;/code&gt;: The original value of &lt;code&gt;displayhook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps1&lt;/code&gt;: The primary prompt string (&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps2&lt;/code&gt;: The continuation prompt string (&lt;code&gt;...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__interactivehook__&lt;/code&gt;: Called when an interactive session starts up&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_baserepl()&lt;/code&gt;: Starts the basic fallback REPL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.interpreter&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Information about the Python build, the Python installation, and the operating system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;platform&lt;/code&gt;: Platform identifier string (&lt;code&gt;linux&lt;/code&gt;, &lt;code&gt;darwin&lt;/code&gt;, &lt;code&gt;win32&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;version&lt;/code&gt;: Python version string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;version_info&lt;/code&gt;: Python version as a named tuple&lt;/li&gt;
&lt;li&gt;&lt;code&gt;implementation&lt;/code&gt;: Python implementation details (CPython, PyPy, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;executable&lt;/code&gt;: Path to the Python interpreter&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prefix&lt;/code&gt;, &lt;code&gt;exec_prefix&lt;/code&gt;: Installation prefixes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base_prefix&lt;/code&gt;, &lt;code&gt;base_exec_prefix&lt;/code&gt;: Installation prefixes, ignoring virtual environments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;platlibdir&lt;/code&gt;: Platform-specific library directory name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxunicode&lt;/code&gt;: Maximum Unicode code point (1114111)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxsize&lt;/code&gt;: Maximum size of containers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;byteorder&lt;/code&gt;: Native byte order (&lt;code&gt;'little'&lt;/code&gt; or &lt;code&gt;'big'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hexversion&lt;/code&gt;: Version encoded as single integer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api_version&lt;/code&gt;: C API version&lt;/li&gt;
&lt;li&gt;&lt;code&gt;copyright&lt;/code&gt;: Python copyright notice&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abiflags&lt;/code&gt;: ABI flags from PEP 3149&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abi_info&lt;/code&gt;: ABI details namespace (Python 3.15+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float_info&lt;/code&gt;: Floating point implementation details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int_info&lt;/code&gt;: Integer implementation details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hash_info&lt;/code&gt;: Hash algorithm parameters&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float_repr_style&lt;/code&gt;: float &lt;code&gt;repr&lt;/code&gt; style (&lt;code&gt;'short'&lt;/code&gt; or &lt;code&gt;'legacy'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;winver&lt;/code&gt;, &lt;code&gt;dllhandle&lt;/code&gt;, &lt;code&gt;getwindowsversion()&lt;/code&gt;: Windows-specific details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getandroidapilevel()&lt;/code&gt;: Android-specific detail&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_base_executable&lt;/code&gt;, &lt;code&gt;_framework&lt;/code&gt;, &lt;code&gt;_git&lt;/code&gt;, &lt;code&gt;_home&lt;/code&gt;, &lt;code&gt;_stdlib_dir&lt;/code&gt;: Assorted build and installation details&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.memory&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Memory management tools used for profiling, optimization, and debugging:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getsizeof()&lt;/code&gt;: Size of an object in bytes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getrefcount()&lt;/code&gt;: Number of references to an object&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getallocatedblocks()&lt;/code&gt;: Number of allocated memory blocks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getunicodeinternedsize()&lt;/code&gt;: Number of interned strings&lt;/li&gt;
&lt;li&gt;&lt;code&gt;intern()&lt;/code&gt;: Intern a string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_is_interned()&lt;/code&gt;: Check whether a string is interned&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_is_immortal()&lt;/code&gt;: Check whether an object is immortal&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_debugmallocstats()&lt;/code&gt;: Print memory allocator statistics&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_clear_type_cache()&lt;/code&gt;, &lt;code&gt;_clear_internal_caches()&lt;/code&gt;: Clear interpreter caches&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.exceptions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Tools for accessing and handling exceptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exc_info()&lt;/code&gt;: Currently handled exception, as a 3-tuple&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exception()&lt;/code&gt;: Currently handled exception (Python 3.11+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;last_exc&lt;/code&gt;, &lt;code&gt;last_type&lt;/code&gt;, &lt;code&gt;last_value&lt;/code&gt;, &lt;code&gt;last_traceback&lt;/code&gt;: The most recent unhandled exception, mostly for REPL use&lt;/li&gt;
&lt;li&gt;&lt;code&gt;excepthook()&lt;/code&gt;: Called to display unhandled exceptions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__excepthook__&lt;/code&gt;: The original value of &lt;code&gt;excepthook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unraisablehook()&lt;/code&gt;: Called for exceptions that can&amp;rsquo;t be raised&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__unraisablehook__&lt;/code&gt;: The original value of &lt;code&gt;unraisablehook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tracebacklimit&lt;/code&gt;: Maximum number of traceback levels to display&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.profile&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Profiling, tracing, auditing, and other runtime introspection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setprofile()&lt;/code&gt;, &lt;code&gt;getprofile()&lt;/code&gt;: Profiling hooks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_setprofileallthreads()&lt;/code&gt;: Set profile function for all threads (Python 3.12+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;settrace()&lt;/code&gt;, &lt;code&gt;gettrace()&lt;/code&gt;: Tracing hooks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_settraceallthreads()&lt;/code&gt;: Set trace function for all threads (Python 3.12+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;call_tracing()&lt;/code&gt;: Call a function with tracing enabled&lt;/li&gt;
&lt;li&gt;&lt;code&gt;monitoring&lt;/code&gt;: Low-overhead monitoring events namespace (Python 3.12+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_getframe()&lt;/code&gt;, &lt;code&gt;_getframemodulename()&lt;/code&gt;: Frame inspection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_current_frames()&lt;/code&gt;, &lt;code&gt;_current_exceptions()&lt;/code&gt;: Frames and exceptions across all threads&lt;/li&gt;
&lt;li&gt;&lt;code&gt;activate_stack_trampoline()&lt;/code&gt;, &lt;code&gt;deactivate_stack_trampoline()&lt;/code&gt;, &lt;code&gt;is_stack_trampoline_active()&lt;/code&gt;: Support for the perf profiler (Python 3.12+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audit()&lt;/code&gt;: Raise an auditing event&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addaudithook()&lt;/code&gt;: Register an audit hook&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remote_exec()&lt;/code&gt;, &lt;code&gt;is_remote_debug_enabled()&lt;/code&gt;: Remote debugging support (Python 3.14+)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;sys.runtime&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Settings that control interpreter runtime behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setrecursionlimit()&lt;/code&gt;, &lt;code&gt;getrecursionlimit()&lt;/code&gt;: Recursion depth control&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setswitchinterval()&lt;/code&gt;, &lt;code&gt;getswitchinterval()&lt;/code&gt;: Thread switching control&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_finalizing()&lt;/code&gt;: Whether the interpreter is shutting down&lt;/li&gt;
&lt;li&gt;&lt;code&gt;breakpointhook()&lt;/code&gt;: Called by the built-in &lt;code&gt;breakpoint&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__breakpointhook__&lt;/code&gt;: The original value of &lt;code&gt;breakpointhook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dont_write_bytecode&lt;/code&gt;: Suppress &lt;code&gt;.pyc&lt;/code&gt; file generation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;warnoptions&lt;/code&gt;: Warning filter settings&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getfilesystemencoding()&lt;/code&gt;, &lt;code&gt;getfilesystemencodeerrors()&lt;/code&gt;: Filesystem encoding details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getdefaultencoding()&lt;/code&gt;: Default string encoding (always &lt;code&gt;utf-8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_int_max_str_digits()&lt;/code&gt;, &lt;code&gt;set_int_max_str_digits()&lt;/code&gt;: Limit on int-to-string conversion (Python 3.11+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setdlopenflags()&lt;/code&gt;, &lt;code&gt;getdlopenflags()&lt;/code&gt;: Dynamic loading control&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thread_info&lt;/code&gt;: Thread implementation details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_is_gil_enabled()&lt;/code&gt;: Whether the GIL is enabled (Python 3.13+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_jit&lt;/code&gt;: JIT compiler introspection namespace&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_dump_tracelets()&lt;/code&gt;: Dump the JIT&amp;rsquo;s internal tracelets&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_get_cpu_count_config()&lt;/code&gt;: Configured CPU count override&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_asyncgen_hooks()&lt;/code&gt;, &lt;code&gt;set_asyncgen_hooks()&lt;/code&gt;: Async generator lifecycle hooks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_coroutine_origin_tracking_depth()&lt;/code&gt;, &lt;code&gt;set_coroutine_origin_tracking_depth()&lt;/code&gt;: Coroutine debugging depth&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_enablelegacywindowsfsencoding()&lt;/code&gt;: Windows mbcs encoding compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Could this actually be done?&lt;/h2&gt;
&lt;p&gt;When I first pondered this experiment last year, this was a very hypothetical thought experiment that I assumed could never be done.
I still &lt;em&gt;mostly&lt;/em&gt; feel the same way.&lt;/p&gt;
&lt;p&gt;There are a few big problems with such a refactoring:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What would the transition period look like for the &lt;em&gt;huge&lt;/em&gt; amount of code that currently uses existing &lt;code&gt;sys&lt;/code&gt; features?&lt;/li&gt;
&lt;li&gt;Would backwards compatibility be maintained forever? If so, would this cause more confusion than it&amp;rsquo;s worth?&lt;/li&gt;
&lt;li&gt;How would code that monkey patches attributes like &lt;code&gt;sys.stdout&lt;/code&gt; work?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I thought the third question was the biggest roadblock, but I now think it&amp;rsquo;s the first 2 questions.&lt;/p&gt;
&lt;p&gt;Those first 2 questions are big questions and I haven&amp;rsquo;t thoroughly thought through the upsides and downsides of such a refactoring.&lt;/p&gt;
&lt;p&gt;That third question is a technical one, but I &lt;em&gt;think&lt;/em&gt; I can answer it&amp;hellip; but the answer is messy.&lt;/p&gt;
&lt;h2&gt;The magic of module-level &lt;code&gt;__setattr__&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;When Python users want to capture all output from their program to a file, they reassign &lt;code&gt;sys.stdout&lt;/code&gt; to an in-memory file-like object.
That&amp;rsquo;s how the &lt;a href="https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout"&gt;&lt;code&gt;contextlib.redirect_stdout&lt;/code&gt;&lt;/a&gt; helper works, and many testing tools use the same technique.&lt;/p&gt;
&lt;p&gt;This monkey patching of &lt;code&gt;sys.stdout&lt;/code&gt; is &lt;strong&gt;somewhat common&lt;/strong&gt;, which poses a bit of a problem for us.&lt;/p&gt;
&lt;p&gt;Imagine that &lt;code&gt;stdout&lt;/code&gt; &lt;em&gt;actually&lt;/em&gt; lived in a &lt;code&gt;sys.io&lt;/code&gt; submodule.
If &lt;code&gt;sys.stdout&lt;/code&gt; and &lt;code&gt;sys.io.stdout&lt;/code&gt; were two separate attributes, code that assigned to one would be invisible to code that read from the other.&lt;/p&gt;
&lt;p&gt;We need a way to synchronize reads and writes of the old flat &lt;code&gt;sys&lt;/code&gt; namespace to &lt;strong&gt;forward them&lt;/strong&gt; to the newly nested namespace within the right submodule.&lt;/p&gt;
&lt;p&gt;Python has supported customizing module-level attribute &lt;em&gt;reads&lt;/em&gt; since Python 3.7, thanks to module-level &lt;code&gt;__getattr__&lt;/code&gt; functions (&lt;a href="https://peps.python.org/pep-0562/"&gt;PEP 562&lt;/a&gt;).
But Python doesn&amp;rsquo;t support module-level &lt;code&gt;__setattr__&lt;/code&gt; functions (that idea was proposed in &lt;a href="https://peps.python.org/pep-0726/"&gt;PEP 726&lt;/a&gt; and rejected).&lt;/p&gt;
&lt;p&gt;Although&amp;hellip; every Python module is an instance of &lt;code&gt;ModuleType&lt;/code&gt;, and &lt;strong&gt;Python allows changing the class of a module object&lt;/strong&gt;.
If we swap in a &lt;code&gt;ModuleType&lt;/code&gt; subclass, we can define whatever &lt;code&gt;__getattr__&lt;/code&gt; and &lt;code&gt;__setattr__&lt;/code&gt; behavior we&amp;rsquo;d like.&lt;/p&gt;
&lt;p&gt;This trick is demonstrated in a proof-of-concept &lt;a href="https://github.com/treyhunner/newsys"&gt;&lt;code&gt;newsys&lt;/code&gt; package&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;newsys&lt;/code&gt; package reimplements &lt;code&gt;sys&lt;/code&gt; as a package with 9 submodules.
As a proof of concept, this module simply proxies to the &lt;code&gt;sys&lt;/code&gt; module.
All reads and writes of &lt;code&gt;newsys.io.stdout&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; &lt;code&gt;newsys.stdout&lt;/code&gt; proxy to the original &lt;code&gt;sys.stdout&lt;/code&gt; (since that&amp;rsquo;s what everything else still uses under the hood in the existing Python interpreter).&lt;/p&gt;
&lt;h2&gt;What would the transition look like?&lt;/h2&gt;
&lt;p&gt;If this transition was ever &lt;em&gt;actually&lt;/em&gt; done, I imagine it might look something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the submodules, with every old flat name still working via forwarding&lt;/li&gt;
&lt;li&gt;Update the documentation to nudge folks toward the new names&lt;/li&gt;
&lt;li&gt;Soft deprecate the flat names someday (or maybe never)&lt;/li&gt;
&lt;li&gt;(Likely never) hard deprecate the flat names&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;em&gt;tiny bit&lt;/em&gt; of precedent for &lt;code&gt;sys&lt;/code&gt; submodules: &lt;code&gt;sys.monitoring&lt;/code&gt; (added in Python 3.12) is an actual module that lives under &lt;code&gt;sys&lt;/code&gt;.
But since &lt;code&gt;sys&lt;/code&gt; isn&amp;rsquo;t a package, &lt;code&gt;import sys.monitoring&lt;/code&gt; doesn&amp;rsquo;t work, as noted at the top of the &lt;a href="https://docs.python.org/3/library/sys.monitoring.html"&gt;&lt;code&gt;sys.monitoring&lt;/code&gt; documentation page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But, I doubt this will ever be done.
Python isn&amp;rsquo;t known for reorganizing modules just to clean things up (outside of the big Python 2/3 split).&lt;/p&gt;
&lt;p&gt;Removing names like &lt;code&gt;sys.path&lt;/code&gt; and &lt;code&gt;sys.argv&lt;/code&gt; would break a &lt;em&gt;huge&lt;/em&gt; amount of code&amp;hellip; and I can imagine Python tutorials and long-time Python users dragging their feet on re-learning &amp;ldquo;the new way&amp;rdquo;.
After all&amp;hellip; why re-learn something when the old version already works and isn&amp;rsquo;t going anywhere?&lt;/p&gt;
&lt;p&gt;If this was ever done, the flat names might need to keep working &lt;em&gt;forever&lt;/em&gt;, and permanent aliases might cause more confusion than such a reorganization is worth.&lt;/p&gt;
&lt;h2&gt;A thought experiment, not a proposal&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not seriously proposing that we actually reorganize &lt;code&gt;sys&lt;/code&gt;&amp;hellip; at least not seriously enough to draft a PEP.&lt;/p&gt;
&lt;p&gt;But I do think there&amp;rsquo;s a practical takeaway here for our own code.
When a &lt;code&gt;utils&lt;/code&gt; module grows out of hand, submodules can help.
And if other code relies on the old flat names, a module-level &lt;code&gt;__getattr__&lt;/code&gt; function (or that hacky &lt;code&gt;__class__&lt;/code&gt; trick) can keep the old names working while you reorganize.&lt;/p&gt;
&lt;p&gt;I doubt &lt;code&gt;sys&lt;/code&gt; will ever change, but I had fun imagining a version of Python where it did.&lt;/p&gt;</content><category term="python"/></entry><entry><title>Using a ~/.pdbrc file to customize the Python Debugger</title><link href="https://treyhunner.com/2026/04/customizing-pdb-with-pdbrc/" rel="alternate"/><published>2026-04-06T07:30:00-07:00</published><updated>2026-04-06T07:30:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2026/04/customizing-pdb-with-pdbrc</id><content type="html">&lt;p&gt;Did you know that you can customize the Python debugger (PDB) by creating custom aliases within a &lt;code&gt;.pdbrc&lt;/code&gt; file in your home directory or Python&amp;rsquo;s current working directory?&lt;/p&gt;
&lt;p&gt;I recently learned this and I&amp;rsquo;d like to share a few helpful aliases that I now have access to in my PDB sessions thanks to my new &lt;code&gt;~/.pdbrc&lt;/code&gt; file.&lt;/p&gt;
&lt;h2&gt;The aliases in my &lt;code&gt;~/.pdbrc&lt;/code&gt; file&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s my new &lt;code&gt;~/.pdbrc&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Custom PDB aliases

# dir obj: print non-dunder attributes and methods
alias dir print(*(f&amp;quot;%1.{n} = {v!r}&amp;quot; for n, v in __import__(&amp;#39;inspect&amp;#39;).getmembers(%1) if not n.startswith(&amp;quot;__&amp;quot;)), sep=&amp;quot;\n&amp;quot;)

# attrs obj: print non-dunder data attributes
alias attrs import inspect as __i ;; print(*(f&amp;quot;%1.{n} = {v!r}&amp;quot; for n, v in __i.getmembers(%1, lambda v: not __i.isroutine(v)) if not n.startswith(&amp;quot;__&amp;quot;)), sep=&amp;quot;\n&amp;quot;) ;; del __i

# vars obj: print instance variables (object must have __dict__)
alias vars print(*(f&amp;quot;%1.{k} = {v!r}&amp;quot; for k, v in vars(%1).items()), sep=&amp;quot;\n&amp;quot;)

# src obj: print source file, line number, and code where a class/function was defined
alias src import inspect as __i;; print(f&amp;quot;{__i.getsourcefile(%1)} on line {__i.getsourcelines(%1)[1]}:\n{&amp;#39;&amp;#39;.join(__i.getsource(%1))}&amp;quot;) ;; del __i

# loc: print local variables from current frame
alias loc print(*(f&amp;quot;{name} = {value!r}&amp;quot; for name, value in vars().items() if not name.startswith(&amp;quot;__&amp;quot;)), sep=&amp;quot;\n&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This allows me to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;attrs&lt;/code&gt;, and &lt;code&gt;vars&lt;/code&gt; to inspect Python objects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt; to see the source code of a particular function/class&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loc&lt;/code&gt; to see the local variables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might wonder &amp;ldquo;Can&amp;rsquo;t I use &lt;code&gt;dir(x)&lt;/code&gt; instead of &lt;code&gt;dir x&lt;/code&gt; and &lt;code&gt;vars(x)&lt;/code&gt; instead of &lt;code&gt;vars x&lt;/code&gt; and &lt;code&gt;locals()&lt;/code&gt; instead of &lt;code&gt;loc&lt;/code&gt;?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;You can!&amp;hellip; but those aliases print things out in a nicer format.&lt;/p&gt;
&lt;h2&gt;A demo of these 5 aliases&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s use &lt;code&gt;-m pdb -m calendar&lt;/code&gt; to launch Python&amp;rsquo;s &lt;code&gt;calendar&lt;/code&gt; module from the command line while dropping into PDB immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pdb&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;calendar
&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/calendar.py&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&amp;lt;module&amp;gt;&lt;span class="o"&gt;()&lt;/span&gt;
-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;Calendar printing functions&lt;/span&gt;
&lt;span class="s2"&gt;(Pdb)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then we&amp;rsquo;ll set a breakpoint after lots of stuff has been defined and continue to that breakpoint:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) b 797&lt;/span&gt;
&lt;span class="go"&gt;Breakpoint 1 at /home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/calendar.py:797&lt;/span&gt;
&lt;span class="go"&gt;(Pdb) c&lt;/span&gt;
&lt;span class="go"&gt;&amp;gt; /home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/calendar.py(797)&amp;lt;module&amp;gt;()&lt;/span&gt;
&lt;span class="go"&gt;-&amp;gt; firstweekday = c.getfirstweekday&lt;/span&gt;
&lt;span class="go"&gt;(Pdb) l&lt;/span&gt;
&lt;span class="go"&gt;792&lt;/span&gt;
&lt;span class="go"&gt;793&lt;/span&gt;
&lt;span class="go"&gt;794     # Support for old module level interface&lt;/span&gt;
&lt;span class="go"&gt;795     c = TextCalendar()&lt;/span&gt;
&lt;span class="go"&gt;796&lt;/span&gt;
&lt;span class="go"&gt;797 B-&amp;gt; firstweekday = c.getfirstweekday&lt;/span&gt;
&lt;span class="go"&gt;798&lt;/span&gt;
&lt;span class="go"&gt;799     def setfirstweekday(firstweekday):&lt;/span&gt;
&lt;span class="go"&gt;800         if not MONDAY &amp;lt;= firstweekday &amp;lt;= SUNDAY:&lt;/span&gt;
&lt;span class="go"&gt;801             raise IllegalWeekdayError(firstweekday)&lt;/span&gt;
&lt;span class="go"&gt;802         c.firstweekday = firstweekday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The string representation for that &lt;code&gt;c&lt;/code&gt; variable doesn&amp;rsquo;t tell us much:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) !c&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If we use the &lt;code&gt;dir&lt;/code&gt; alias, we&amp;rsquo;ll see every attribute that&amp;rsquo;s accessible on &lt;code&gt;c&lt;/code&gt; printed in a pretty friendly format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) dir c&lt;/span&gt;
&lt;span class="go"&gt;c._firstweekday = 0&lt;/span&gt;
&lt;span class="go"&gt;c.firstweekday = 0&lt;/span&gt;
&lt;span class="go"&gt;c.formatday = &amp;lt;bound method TextCalendar.formatday of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatmonth = &amp;lt;bound method TextCalendar.formatmonth of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatmonthname = &amp;lt;bound method TextCalendar.formatmonthname of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatweek = &amp;lt;bound method TextCalendar.formatweek of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatweekday = &amp;lt;bound method TextCalendar.formatweekday of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatweekheader = &amp;lt;bound method TextCalendar.formatweekheader of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.formatyear = &amp;lt;bound method TextCalendar.formatyear of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.getfirstweekday = &amp;lt;bound method Calendar.getfirstweekday of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.itermonthdates = &amp;lt;bound method Calendar.itermonthdates of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.itermonthdays = &amp;lt;bound method Calendar.itermonthdays of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.itermonthdays2 = &amp;lt;bound method Calendar.itermonthdays2 of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.itermonthdays3 = &amp;lt;bound method Calendar.itermonthdays3 of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.itermonthdays4 = &amp;lt;bound method Calendar.itermonthdays4 of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.iterweekdays = &amp;lt;bound method Calendar.iterweekdays of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.monthdatescalendar = &amp;lt;bound method Calendar.monthdatescalendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.monthdays2calendar = &amp;lt;bound method Calendar.monthdays2calendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.monthdayscalendar = &amp;lt;bound method Calendar.monthdayscalendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.prmonth = &amp;lt;bound method TextCalendar.prmonth of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.prweek = &amp;lt;bound method TextCalendar.prweek of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.pryear = &amp;lt;bound method TextCalendar.pryear of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.setfirstweekday = &amp;lt;bound method Calendar.setfirstweekday of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.yeardatescalendar = &amp;lt;bound method Calendar.yeardatescalendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.yeardays2calendar = &amp;lt;bound method Calendar.yeardays2calendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c.yeardayscalendar = &amp;lt;bound method Calendar.yeardayscalendar of &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If we use &lt;code&gt;attrs&lt;/code&gt; we&amp;rsquo;ll see &lt;em&gt;just&lt;/em&gt; the non-method attributes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) attrs c&lt;/span&gt;
&lt;span class="go"&gt;c._firstweekday = 0&lt;/span&gt;
&lt;span class="go"&gt;c.firstweekday = 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code&gt;vars&lt;/code&gt; will show us just the attributes that live as proper instance attributes in that object&amp;rsquo;s &lt;code&gt;__dict__&lt;/code&gt; dictionary:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) vars c&lt;/span&gt;
&lt;span class="go"&gt;c._firstweekday = 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; alias can be used to see the source code for a given method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) src c.prmonth&lt;/span&gt;
&lt;span class="go"&gt;/home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/calendar.py on line 404:&lt;/span&gt;
&lt;span class="go"&gt;    def prmonth(self, theyear, themonth, w=0, l=0):&lt;/span&gt;
&lt;span class="go"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;        Print a month&amp;#39;s calendar.&lt;/span&gt;
&lt;span class="go"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;        print(self.formatmonth(theyear, themonth, w, l), end=&amp;#39;&amp;#39;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the &lt;code&gt;loc&lt;/code&gt; alias will show us all the local variables defined in the current scope:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;(Pdb) loc&lt;/span&gt;
&lt;span class="go"&gt;APRIL = __main__.APRIL&lt;/span&gt;
&lt;span class="go"&gt;AUGUST = __main__.AUGUST&lt;/span&gt;
&lt;span class="go"&gt;Calendar = &amp;lt;class &amp;#39;__main__.Calendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;DECEMBER = __main__.DECEMBER&lt;/span&gt;
&lt;span class="go"&gt;Day = &amp;lt;enum &amp;#39;Day&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;FEBRUARY = __main__.FEBRUARY&lt;/span&gt;
&lt;span class="go"&gt;FRIDAY = __main__.FRIDAY&lt;/span&gt;
&lt;span class="go"&gt;HTMLCalendar = &amp;lt;class &amp;#39;__main__.HTMLCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;IllegalMonthError = &amp;lt;class &amp;#39;__main__.IllegalMonthError&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;IllegalWeekdayError = &amp;lt;class &amp;#39;__main__.IllegalWeekdayError&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;IntEnum = &amp;lt;enum &amp;#39;IntEnum&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;JANUARY = __main__.JANUARY&lt;/span&gt;
&lt;span class="go"&gt;JULY = __main__.JULY&lt;/span&gt;
&lt;span class="go"&gt;JUNE = __main__.JUNE&lt;/span&gt;
&lt;span class="go"&gt;LocaleHTMLCalendar = &amp;lt;class &amp;#39;__main__.LocaleHTMLCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;LocaleTextCalendar = &amp;lt;class &amp;#39;__main__.LocaleTextCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;MARCH = __main__.MARCH&lt;/span&gt;
&lt;span class="go"&gt;MAY = __main__.MAY&lt;/span&gt;
&lt;span class="go"&gt;MONDAY = __main__.MONDAY&lt;/span&gt;
&lt;span class="go"&gt;Month = &amp;lt;enum &amp;#39;Month&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;NOVEMBER = __main__.NOVEMBER&lt;/span&gt;
&lt;span class="go"&gt;OCTOBER = __main__.OCTOBER&lt;/span&gt;
&lt;span class="go"&gt;SATURDAY = __main__.SATURDAY&lt;/span&gt;
&lt;span class="go"&gt;SEPTEMBER = __main__.SEPTEMBER&lt;/span&gt;
&lt;span class="go"&gt;SUNDAY = __main__.SUNDAY&lt;/span&gt;
&lt;span class="go"&gt;THURSDAY = __main__.THURSDAY&lt;/span&gt;
&lt;span class="go"&gt;TUESDAY = __main__.TUESDAY&lt;/span&gt;
&lt;span class="go"&gt;TextCalendar = &amp;lt;class &amp;#39;__main__.TextCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;WEDNESDAY = __main__.WEDNESDAY&lt;/span&gt;
&lt;span class="go"&gt;_CLIDemoCalendar = &amp;lt;class &amp;#39;__main__._CLIDemoCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_CLIDemoLocaleCalendar = &amp;lt;class &amp;#39;__main__._CLIDemoLocaleCalendar&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_get_default_locale = &amp;lt;function _get_default_locale at 0x7416de7c0930&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_locale = &amp;lt;module &amp;#39;locale&amp;#39; from &amp;#39;/home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/locale.py&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_localized_day = &amp;lt;class &amp;#39;__main__._localized_day&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_localized_month = &amp;lt;class &amp;#39;__main__._localized_month&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_monthlen = &amp;lt;function _monthlen at 0x7416de7c0720&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_nextmonth = &amp;lt;function _nextmonth at 0x7416de7c0880&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_prevmonth = &amp;lt;function _prevmonth at 0x7416de7c07d0&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;_validate_month = &amp;lt;function _validate_month at 0x7416de7c05c0&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;c = &amp;lt;__main__.TextCalendar object at 0x7416de93af90&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;datetime = &amp;lt;module &amp;#39;datetime&amp;#39; from &amp;#39;/home/trey/.local/share/uv/python/cpython-3.15.0a3-linux-x86_64-gnu/lib/python3.15/datetime.py&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;day_abbr = &amp;lt;__main__._localized_day object at 0x7416de941090&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;day_name = &amp;lt;__main__._localized_day object at 0x7416de93acf0&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;different_locale = &amp;lt;class &amp;#39;__main__.different_locale&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;error = &amp;lt;class &amp;#39;ValueError&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;global_enum = &amp;lt;function global_enum at 0x7416dee17480&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;isleap = &amp;lt;function isleap at 0x7416dea92770&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;leapdays = &amp;lt;function leapdays at 0x7416de7c0460&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]&lt;/span&gt;
&lt;span class="go"&gt;month_abbr = &amp;lt;__main__._localized_month object at 0x7416de9411d0&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;month_name = &amp;lt;__main__._localized_month object at 0x7416de93ae40&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;monthrange = &amp;lt;function monthrange at 0x7416de7c0670&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;repeat = &amp;lt;class &amp;#39;itertools.repeat&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;standalone_month_abbr = &amp;lt;__main__._localized_month object at 0x7416de954fc0&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;standalone_month_name = &amp;lt;__main__._localized_month object at 0x7416de941310&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;sys = &amp;lt;module &amp;#39;sys&amp;#39; (built-in)&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;weekday = &amp;lt;function weekday at 0x7416de7c0510&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;&lt;code&gt;~/.pdbrc&lt;/code&gt; isn&amp;rsquo;t as powerful as &lt;code&gt;PYTHONSTARTUP&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I also have a custom &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file, which is launched every time I start a new Python REPL (see &lt;a href="https://treyhunner.com/2025/10/handy-python-repl-modifications/"&gt;Handy Python REPL modifications&lt;/a&gt;).
A &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file is just Python code, which makes it easy to customize.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;~/.pdbrc&lt;/code&gt; file is &lt;em&gt;not&lt;/em&gt; Python code&amp;hellip; it&amp;rsquo;s a very limited custom file format.&lt;/p&gt;
&lt;p&gt;You may notice that every &lt;code&gt;alias&lt;/code&gt; line defined in my &lt;code&gt;~/.pdbrc&lt;/code&gt; file is a bunch of code shoved all on one line.
That&amp;rsquo;s because there&amp;rsquo;s no way to define an alias over multiple lines.&lt;/p&gt;
&lt;p&gt;Also any variables assigned in an &lt;code&gt;alias&lt;/code&gt; will leak into the surrounding scope&amp;hellip; so I have a &lt;code&gt;del&lt;/code&gt; statement in a couple of those aliases to clean up a stray variable assignment (from an &lt;code&gt;import&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;See the documentation on &lt;a href="https://docs.python.org/3/library/pdb.html#pdbcommand-alias"&gt;&lt;code&gt;alias&lt;/code&gt;&lt;/a&gt; and the top of the &lt;a href="https://docs.python.org/3/library/pdb.html#debugger-commands"&gt;debugger commands&lt;/a&gt; for more on how &lt;code&gt;~/.pdbrc&lt;/code&gt; files work.&lt;/p&gt;</content><category term="python"/></entry><entry><title>Python Black Friday &amp; Cyber Monday sales (2025)</title><link href="https://treyhunner.com/2025/11/python-black-friday-and-cyber-monday-sales-2025/" rel="alternate"/><published>2025-11-24T09:00:00-07:00</published><updated>2025-11-24T09:00:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/11/python-black-friday-and-cyber-monday-sales-2025</id><content type="html">&lt;p&gt;It&amp;rsquo;s time for some &lt;strong&gt;discounted&lt;/strong&gt; Python-related skill-building.
This is my &lt;strong&gt;&lt;a href="https://treyhunner.com/blog/categories/sales/"&gt;eighth annual compilation&lt;/a&gt;&lt;/strong&gt; of Python learning-related Black Friday &amp;amp; Cyber Monday deals.
If you find a Python-related deal in the next week that isn&amp;rsquo;t on this list, please contact me.&lt;/p&gt;
&lt;h2&gt;Python-related sales&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s not even Black Friday yet, but most of these Python-related Black Friday sales are &lt;strong&gt;already live&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pythonmorsels.com/lifetime-access-sale"&gt;Python Morsels&lt;/a&gt;&lt;/strong&gt;: I&amp;rsquo;m offering &lt;strong&gt;lifetime access&lt;/strong&gt; for the second time ever (more details below)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://courses.dataschool.io/pass"&gt;Data School&lt;/a&gt;&lt;/strong&gt;: a new subscription to access all of Kevin&amp;rsquo;s &lt;strong&gt;7 courses plus all upcoming courses&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://talkpython.fm/black-friday"&gt;Talk Python&lt;/a&gt;&lt;/strong&gt;: AI Python bundle, the Everything Bundle, and Michael&amp;rsquo;s Talk Python in Production&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://lernerpython.com/pricing/"&gt;Reuven Lerner&lt;/a&gt;&lt;/strong&gt;: get 20% off &lt;strong&gt;your first year&lt;/strong&gt; of the &lt;strong&gt;LernerPython+data&lt;/strong&gt; tier (code &lt;code&gt;BF2025&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://pythontest.com/2025-black-friday/"&gt;Brian Okken&lt;/a&gt;&lt;/strong&gt;: get 50% off his pytest books and courses (code &lt;code&gt;SAVE50&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://mathspp.gumroad.com/l/all-books-bundle/BF202550"&gt;Rodrigo&lt;/a&gt;&lt;/strong&gt;: get 50% off all his books including his all books bundle (code &lt;code&gt;BF202550&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.blog.pythonlibrary.org/2025/11/18/black-friday-python-deals-came-early/"&gt;Mike Driscoll&lt;/a&gt;&lt;/strong&gt;: get 50% off all his &lt;strong&gt;Python books and courses&lt;/strong&gt; (code &lt;code&gt;BLACKISBACK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://thepythoncodingplace.thinkific.com/bundles/the-python-coding-place-membership?coupon=black50"&gt;The Python Coding Place&lt;/a&gt;&lt;/strong&gt;: get 50% the &lt;strong&gt;all course bundle&lt;/strong&gt; of 12 courses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://learnbyexample.gumroad.com/l/all-books/FestiveOffer"&gt;Sundeep Agarwal&lt;/a&gt;&lt;/strong&gt;: ~55% off Sundeep&amp;rsquo;s &lt;strong&gt;all books bundle&lt;/strong&gt; with code &lt;code&gt;FestiveOffer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nodeledge.ai/learn/paths/essential-python-for-data-science-and-ml"&gt;Nodeledge&lt;/a&gt;&lt;/strong&gt;: get 20% off your first payment (code &lt;code&gt;BF2025&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.oreilly.com/online-learning/2025-cyber-sale.html?code=CYBERSAVE25"&gt;O&amp;rsquo;Reilly Media&lt;/a&gt;&lt;/strong&gt;: 40% off &lt;strong&gt;the first year&lt;/strong&gt; with code &lt;code&gt;CYBERWEEK25&lt;/code&gt; ($299 instead of $499)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.manning.com/catalog/programming-languages-and-styles/python"&gt;Manning&lt;/a&gt;&lt;/strong&gt; is offering 50% off from Nov 25 to Dec 1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://mastodon.social/@b0rk@jvns.ca/115571664177415123"&gt;Wizard Zines&lt;/a&gt;&lt;/strong&gt;: 50% all of Julia Evan&amp;rsquo;s &lt;em&gt;great&lt;/em&gt; &lt;strong&gt;zines on various tech topics&lt;/strong&gt; that I personally find both fun and useful (not Python-related, but one of my favorite annual sales)&amp;hellip; this is a one-day Black Friday exclusive sale&lt;/li&gt;
&lt;/ul&gt;
&lt;!--
Likely additional sales:

- **[Brian Okken](https://courses.pythontest.com)**
- **[Matt Harrison][]**
- **[Rodrigo][]**
- **[Test Driven][]**
- **[Pragmatic Bookshelf][]**
- **[No Starch][]**
- **[Manning][]**
--&gt;

&lt;p&gt;I will be keeping an idea on other potential sales and updating this post as I find them.
If you&amp;rsquo;ve seen a sale that I haven&amp;rsquo;t, please contact me or comment below.&lt;/p&gt;
&lt;h2&gt;Django-related sales&lt;/h2&gt;
&lt;p&gt;Adam Johnson has also published a list of &lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/"&gt;Django-related deals for Black Friday&lt;/a&gt; (which he&amp;rsquo;s been doing for a few years now).
I&amp;rsquo;ve included links below to the different sections in Adam&amp;rsquo;s post&amp;hellip;&lt;/p&gt;
&lt;p&gt;Courses and books:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#my-books"&gt;Adam&amp;rsquo;s books&lt;/a&gt;: his books and bundles on Django, Git, and GitHub DX and Django test performance are all 50% off&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#learndjango-com-course-bundle"&gt;LearnDjango.com course bundle&lt;/a&gt;: Will Vincent is offering 50% off his 3 courses&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#django-5-by-example"&gt;Django 5 By Example&lt;/a&gt;: 30% off this book by Antonio Melé&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#django-in-action"&gt;Django in Action&lt;/a&gt;: 50% off this book by Christopher Trudeau&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#async-patterns-in-django"&gt;Async Patterns in Django&lt;/a&gt;: 50% off this book by Paul Bailey&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Packages related to Django:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#aidas-bendoraitis-paid-packages"&gt;Aidas Bendoraitis&lt;/a&gt;: has 3 Django packages available at a 20% discount&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#appliku"&gt;Appliku&lt;/a&gt;: 30% of annual plans for this Django deployment tool&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#saas-pegasus"&gt;Saas Pegasus&lt;/a&gt;: 50% off the unlimited license for this configurable Django project template&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Adam also gives a shout out to &lt;a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#bonus-django-itself"&gt;sponsoring Django itself&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;More on my sale: Python Morsels Lifetime Access&lt;/h2&gt;
&lt;p&gt;Python Morsels is a hands-on, &lt;strong&gt;exercise-driven&lt;/strong&gt; Python skill-building platform designed to help developers &lt;strong&gt;write cleaner, more idiomatic Python&lt;/strong&gt; through real-world practice.
This growing library of exercises, videos, and courses is aimed primarily at intermediate and professional developers.
If you haven&amp;rsquo;t used Python Morsels, you can read about it &lt;a href="https://treyhunner.com/2025/11/lifetime-access-sale-2025/"&gt;in my sale announcement post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This Black Friday / Cyber Monday, I&amp;rsquo;m offering &lt;a href="https://www.pythonmorsels.com/lifetime-access-sale"&gt;lifetime access&lt;/a&gt;: all current and future content for in a single payment.
If you&amp;rsquo;d like to build confidence in your everyday Python skills, consider this sale.
It&amp;rsquo;s &lt;strong&gt;about 50% cheaper than paying annually for five years&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pythonmorsels.com/lifetime-access-sale/" class="subscribe-btn form-big"&gt;Get lifetime access to Python Morsels&lt;/a&gt;&lt;/p&gt;</content><category term="python"/><category term="sales"/></entry><entry><title>Python Morsels Lifetime Access Sale</title><link href="https://treyhunner.com/2025/11/lifetime-access-sale-2025/" rel="alternate"/><published>2025-11-21T14:42:36-08:00</published><updated>2025-11-21T14:42:36-08:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/11/lifetime-access-sale-2025</id><content type="html">&lt;p&gt;If you code in Python regularly, you&amp;rsquo;re already learning new things everyday.&lt;/p&gt;
&lt;p&gt;You hit a wall, or something breaks. Then you search around, spend some hours on Stack Overflow, and eventually, you figure it out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But this kind of learning is unstructured. It&amp;rsquo;s reactive, instead of intentional.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You fix the problem at hand, but the underlying gaps in your knowledge remain unaddressed.&lt;/p&gt;
&lt;h2&gt;A more structured way to improve your Python skills&lt;/h2&gt;
&lt;p&gt;Python Morsels gives you &lt;strong&gt;a structured, hands-on way to improve your Python skills&lt;/strong&gt; through weekly practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notice significant progress with as little as 30 minutes a week&lt;/li&gt;
&lt;li&gt;Learn to naturally think more Pythonically&lt;/li&gt;
&lt;li&gt;Explore new approaches to problem-solving&lt;/li&gt;
&lt;li&gt;Challenge yourself to get outside your comfort zone regularly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Python Morsels is a subscription service because &lt;strong&gt;I&amp;rsquo;m adding new learning resources almost every week&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But through December 1st, you can get &lt;strong&gt;&lt;a href="https://pythonmorsels.com/lifetime-access-sale/"&gt;lifetime access&lt;/a&gt;&lt;/strong&gt; for a one-time payment.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;When you sign up for Python Morsels, you&amp;rsquo;ll choose your current Python skill level, from novice to advanced.&lt;/p&gt;
&lt;p&gt;Based on your skill level, &lt;strong&gt;each Monday I&amp;rsquo;ll send you a personalized routine&lt;/strong&gt; with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a short screencast to watch (or read)&lt;/li&gt;
&lt;li&gt;a multi-part exercise to move you outside your comfort zone&lt;/li&gt;
&lt;li&gt;a mini exercise that you can accomplish in just 10 minutes&lt;/li&gt;
&lt;li&gt;links to dive deeper into subsequent screencasts and exercises&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of Python Morsels as a gym for your Python skills: you come in for quick training sessions, put in the reps, and make a little more progress each time.&lt;/p&gt;
&lt;p&gt;All these resources are accessible to you forever, and with lifetime access you&amp;rsquo;ll never pay another subscription fee.&lt;/p&gt;
&lt;h2&gt;What Python Morsels includes&lt;/h2&gt;
&lt;p&gt;Python Morsels has grown a lot over the past 8 years. Currently, Python Morsels has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;235+ Video Lessons (or screencasts, as I call them)&lt;/li&gt;
&lt;li&gt;262+ Hands-On Exercises (with solution walkthroughs)&lt;/li&gt;
&lt;li&gt;500+ Optional Bonuses (to challenge every skill level)&lt;/li&gt;
&lt;li&gt;303+ Articles Organized Topic-Wise (if you prefer reading)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll be sending you personalized recommendations every week, but you can use these resources however they fit your routine: as learning guides, hands-on practice sessions, quick cheatsheets, long-term reference material, or quick Python workouts.&lt;/p&gt;
&lt;p&gt;In addition to this, Python Morsels also gives you access to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python Jumpstart - a structured course for beginners&lt;/li&gt;
&lt;li&gt;Additional Deep-Dive Courses - structured tracks to master a concept&lt;/li&gt;
&lt;li&gt;Exercise Paths - topic-based exercises to strengthen a specific skill&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because Python Morsels runs as an active subscription service, I&amp;rsquo;m always adding new screencasts, new exercises, and updated material on a weekly or monthly cycle. I also keep everything up-to-date with each new Python release, incorporating newly added features and retiring end-of-life&amp;rsquo;d Python versions.&lt;/p&gt;
&lt;h2&gt;Lock in lifetime access&lt;/h2&gt;
&lt;p&gt;Python Morsels usually costs &lt;strong&gt;$240/year&lt;/strong&gt; but you can get &lt;strong&gt;lifetime access&lt;/strong&gt; through December 1st for a one-time payment. I&amp;rsquo;ve only offered lifetime access &lt;strong&gt;once before&lt;/strong&gt; in 8 years.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been on the fence about subscribing to Python Morsels or want to invest in building a daily learning habit, this is a good time to do it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pythonmorsels.com/lifetime-access-sale/" class="subscribe-btn form-big"&gt;Get lifetime access to Python Morsels&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have questions about the sale, please comment below or &lt;a href="mailto:he&amp;#108;p&amp;#64;&amp;#112;%7&amp;#57;th%6Fnmo&amp;#114;s%6&amp;#53;ls&amp;#46;&amp;#99;&amp;#111;m"&gt;email me&lt;/a&gt;.&lt;/p&gt;</content><category term="python"/><category term="sales"/></entry><entry><title>Handy Python REPL Modifications</title><link href="https://treyhunner.com/2025/10/handy-python-repl-modifications/" rel="alternate"/><published>2025-10-08T19:59:20-07:00</published><updated>2025-10-08T19:59:20-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/10/handy-python-repl-modifications</id><content type="html">&lt;p&gt;I find myself in the Python REPL &lt;em&gt;a lot&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I open up the REPL to play with an idea, to use Python as a calculator or quick and dirty text parsing tool, to record a screencast, to come up with a code example for an article, and (most importantly for me) to teach Python.
My Python courses and workshops are based largely around writing code together to guess how something works, try it out, and repeat.&lt;/p&gt;
&lt;p&gt;As I&amp;rsquo;ve written about before, you can &lt;a href="https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/"&gt;add custom keyboard shortcuts&lt;/a&gt; to the new Python REPL (since 3.13) and &lt;a href="https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/"&gt;customize the REPL syntax highlighting&lt;/a&gt; (since 3.14).
If you spend time in the Python REPL and wish it behaved a little more &lt;strong&gt;like your favorite editor&lt;/strong&gt;, these tricks can come in handy.&lt;/p&gt;
&lt;p&gt;I have added &lt;strong&gt;custom keyboard shortcuts&lt;/strong&gt; to my REPL and other modifications to help me &lt;strong&gt;more quickly write and edit code in my REPL&lt;/strong&gt;.
I&amp;rsquo;d like to share some of the modifications that I&amp;rsquo;ve found helpful in my own Python REPL.&lt;/p&gt;
&lt;h2&gt;Creating a PYTHONSTARTUP file&lt;/h2&gt;
&lt;p&gt;If you want to run Python code every time an interactive prompt (a REPL) starts, you can make a PYTHONSTARTUP file.&lt;/p&gt;
&lt;p&gt;When Python launches an interactive prompt, it checks for a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable.
If it finds one, it treats it as a filename that contains Python code and it &lt;strong&gt;runs all the code in that file&lt;/strong&gt;, as if you had copy-pasted the code into the REPL.&lt;/p&gt;
&lt;p&gt;So all of the modifications I have made to my Python REPL rely on this &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; variable in my &lt;code&gt;~/.zshenv&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONSTARTUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.startup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you use bash, you&amp;rsquo;ll put that in your &lt;code&gt;~/.bashrc&lt;/code&gt; file.
If you&amp;rsquo;re on Windows &lt;a href="https://gist.github.com/mitchmindtree/92c8e37fa80c8dddee5b94fc88d1288b"&gt;you&amp;rsquo;ll need to set an environment variable the Windows way&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With that variable set, I can now create a &lt;code&gt;~/.startup.py&lt;/code&gt; file that has Python code in it.
That code will automatically run every time I launch a new Python REPL, whether within a virtual environment or outside of one.&lt;/p&gt;
&lt;h2&gt;My REPL keyboard shortcuts&lt;/h2&gt;
&lt;p&gt;The quick summary of my &lt;em&gt;current&lt;/em&gt; modifications are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pressing &lt;strong&gt;Home&lt;/strong&gt; moves to the &lt;strong&gt;first character in the code block&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;End&lt;/strong&gt; moves to the &lt;strong&gt;last character in the code block&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Alt+M&lt;/strong&gt; moves to the &lt;strong&gt;first character&lt;/strong&gt; on the current line&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Shift+Tab&lt;/strong&gt; removes &lt;strong&gt;common indentation&lt;/strong&gt; from the code block&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Alt+Up&lt;/strong&gt; swaps the current line with &lt;strong&gt;the line above it&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Alt+Down&lt;/strong&gt; swaps the current line with &lt;strong&gt;the line below it&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Ctrl+N&lt;/strong&gt; inserts &lt;strong&gt;a specific list of numbers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Pressing &lt;strong&gt;Ctrl+F&lt;/strong&gt; inserts &lt;strong&gt;a specific list of strings&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;ve read &lt;a href="https://www.pythonmorsels.com/repl-features/"&gt;my Python REPL shortcuts&lt;/a&gt; article, you know that we can use &lt;strong&gt;Ctrl+A&lt;/strong&gt; to move to the beginning of the line and &lt;strong&gt;Ctrl+E&lt;/strong&gt; to move to the end of the line.
I already use those instead of the Home and End keys, so I decided to rebind Home and End to do something different.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Alt+M&lt;/strong&gt; key combination is essentially the same as &lt;code&gt;Alt+M&lt;/code&gt; in Emacs or &lt;code&gt;^&lt;/code&gt; in Vim. I usually prefer to move to the beginning of the non-whitespace in a line rather than to the beginning of the &lt;em&gt;entire&lt;/em&gt; line.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Shift+Tab&lt;/strong&gt; functionality is basically a fancy wrapper around &lt;a href="https://www.pythonmorsels.com/dedent/"&gt;using &lt;code&gt;textwrap.dedent&lt;/code&gt;&lt;/a&gt;: it dedents the current code block while keeping the cursor over the same character it was at before.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Ctrl+N&lt;/strong&gt; and &lt;strong&gt;Ctrl+F&lt;/strong&gt; shortcuts make it easier for me to grab an example data structure to work with when teaching.&lt;/p&gt;
&lt;p&gt;In addition to the above changes, I also modify my color scheme to work nicely with my Solarized Light color scheme in Vim.&lt;/p&gt;
&lt;h2&gt;I created a pyrepl-hacks library for this&lt;/h2&gt;
&lt;p&gt;My PYTHONSTARTUP file became so messy that I ended up creating a &lt;a href="https://github.com/treyhunner/pyrepl-hacks"&gt;pyrepl-hacks library&lt;/a&gt; to help me with these modifications.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/treyhunner/dotfiles/commits/main/startup.py"&gt;My PYTHONSTARTUP file&lt;/a&gt; now looks pretty much like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pathlib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_sys&lt;/span&gt;
&lt;span class="n"&gt;_sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_pathlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.pyhacks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pyrepl_hacks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_repl&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# We&amp;#39;re on Python 3.12 or below&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Home&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;home&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;End&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;end&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Alt+M&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;move-to-indentation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Shift+Tab&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;dedent&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Alt+Down&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;move-line-down&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Alt+Up&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;move-line-up&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind_to_insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Ctrl+N&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;[2, 1, 3, 4, 7, 11, 18, 29]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind_to_insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;Ctrl+F&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;[&amp;quot;apples&amp;quot;, &amp;quot;oranges&amp;quot;, &amp;quot;bananas&amp;quot;, &amp;quot;strawberries&amp;quot;, &amp;quot;pears&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Solarized Light theme to match vim&lt;/span&gt;
        &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;green&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;builtin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;blue&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;intense blue&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cyan&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cyan&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;blue&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;soft_keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bold green&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;intense green&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reset, intense green&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# We&amp;#39;re on Python 3.13 or below&lt;/span&gt;

&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_pathlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_sys&lt;/span&gt;  &lt;span class="c1"&gt;# Avoid global REPL namespace pollution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That&amp;rsquo;s pretty short!&lt;/p&gt;
&lt;p&gt;But wait&amp;hellip; won&amp;rsquo;t this fail unless pyrepl-hacks is installed in every virtual environment &lt;em&gt;and&lt;/em&gt; installed globally for every Python version on my machine?&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where that &lt;code&gt;sys.path.append&lt;/code&gt; trick comes in handy&amp;hellip;&lt;/p&gt;
&lt;h2&gt;Wait&amp;hellip; let&amp;rsquo;s acknowledge the dragons 🐲&lt;/h2&gt;
&lt;p&gt;At this point I&amp;rsquo;d like to pause to note that all of this relies on using an implementation detail of Python that is deliberately undocumented because it &lt;em&gt;is not designed&lt;/em&gt; to be used by end users.&lt;/p&gt;
&lt;p&gt;The above code all relies on the &lt;code&gt;_pyrepl&lt;/code&gt; module that was added in Python 3.13 (and optionally the &lt;code&gt;_colorize&lt;/code&gt; module that was added in Python 3.14).&lt;/p&gt;
&lt;p&gt;When I run a new future version of Python (for example Python 3.15) this solution may break.
I&amp;rsquo;m willing to take that risk, as I know that I can always unset my shell&amp;rsquo;s &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; variable or clear out my startup file.&lt;/p&gt;
&lt;p&gt;So, just be aware&amp;hellip; here be (private undocumented implementation detail) dragons.&lt;/p&gt;
&lt;h2&gt;Monkey patching &lt;code&gt;sys.path&lt;/code&gt; to allow importing &lt;code&gt;pyrepl_hacks&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I didn&amp;rsquo;t install pyrepl-hacks &lt;em&gt;the usual way&lt;/em&gt;.
Instead, I installed it in a very specific location.&lt;/p&gt;
&lt;p&gt;I created a &lt;code&gt;~/.pyhacks&lt;/code&gt; directory and then installed &lt;code&gt;pyrepl-hacks&lt;/code&gt; &lt;em&gt;into&lt;/em&gt; that directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/.pyhacks
$&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pyrepl-hacks&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="w"&gt; &lt;/span&gt;~/.pyhacks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In order for the &lt;code&gt;pyrepl_hacks&lt;/code&gt; Python package to work, it needs to available within every Python REPL I might launch.
Normally that would mean that it needs to be installed in every virtual environment that Python runs within.
This trick avoids that constraint.&lt;/p&gt;
&lt;p&gt;When Python tries to import a module, it iterates through the &lt;code&gt;sys.path&lt;/code&gt; directory list.
Any Python packages found &lt;em&gt;within&lt;/em&gt; any of the &lt;code&gt;sys.path&lt;/code&gt; directories may be imported.&lt;/p&gt;
&lt;p&gt;So monkey patching &lt;code&gt;sys.path&lt;/code&gt; within my PYTHONSTARTUP file allows &lt;code&gt;pyrepl_hacks&lt;/code&gt; to be imported in every Python interpreter I launch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.pyhacks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With those 3 lines (or something like them) placed in my PYTHONSTARTUP file, all interactive Python interpreters I launch will be able to import modules that are located in my &lt;code&gt;~/.pyhacks&lt;/code&gt; directory.&lt;/p&gt;
&lt;h2&gt;Creating your own custom REPL commands&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s pretty neat.
But what if you want to invent your own REPL commands?&lt;/p&gt;
&lt;p&gt;Well, the &lt;code&gt;bind&lt;/code&gt; utility I&amp;rsquo;ve created in the &lt;code&gt;pyrepl_hacks&lt;/code&gt; module can be used as a decorator for that.&lt;/p&gt;
&lt;p&gt;This will make Ctrl+X followed by Ctrl+R insert &lt;code&gt;import subprocess&lt;/code&gt; followed by &lt;code&gt;subprocess.run("", shell=True)&lt;/code&gt; with the cursor positioned in between the double quotes after it&amp;rsquo;s all inserted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pyrepl_hacks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_repl&lt;/span&gt;

&lt;span class="nd"&gt;@_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Ctrl+X Ctrl+R&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;subprocess_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Ctrl+X followed by Ctrl+R will insert a subprocess.run command.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import subprocess&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;subprocess.run(&amp;quot;&amp;quot;, shell=True)&amp;#39;&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;_repl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can read more about the ins and outs of the pyrepl-hacks package &lt;a href="https://github.com/treyhunner/pyrepl-hacks#readme"&gt;in the readme file&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;pyrepl-hacks is just a fancy wrapper&lt;/h2&gt;
&lt;p&gt;The pyrepl-hacks package is really just a fancy wrapper around Python&amp;rsquo;s &lt;code&gt;_pyrepl&lt;/code&gt; and &lt;code&gt;_colorize&lt;/code&gt; modules.&lt;/p&gt;
&lt;p&gt;Why did I make a whole package and then modify my &lt;code&gt;sys.path&lt;/code&gt; to use it, when I could have just used &lt;code&gt;_pyrepl&lt;/code&gt; directly?&lt;/p&gt;
&lt;p&gt;Three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;To make creating new commands &lt;em&gt;a bit&lt;/em&gt; easier (functions can be used instead of classes)&lt;/li&gt;
&lt;li&gt;To make the key bindings look a bit nicer (I prefer &lt;code&gt;"Ctrl+M"&lt;/code&gt; over &lt;code&gt;r"\C-M"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;To hide my hairy hacks behind a shiny API ✨&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before I made pyrepl-hacks, I implemented these commands directly within my PYTHONSTARTUP file by reaching into the internals of &lt;code&gt;_pyrepl&lt;/code&gt; directly.&lt;/p&gt;
&lt;p&gt;My PYTHONSTARTUP file before pyrepl-hacks was &lt;a href="https://pym.dev/p/35q9e/"&gt;over 100 lines longer&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Try pyrepl-hacks and leave feedback&lt;/h2&gt;
&lt;p&gt;My hope is that the &lt;a href="https://github.com/treyhunner/pyrepl-hacks"&gt;pyrepl-hacks&lt;/a&gt; library will be obsolete one day.
Eventually the &lt;code&gt;_pyrepl&lt;/code&gt; module might be renamed to &lt;code&gt;pyrepl&lt;/code&gt; (or maybe just &lt;code&gt;repl&lt;/code&gt;?) and it will have a well-documented friendly-ish public interface.&lt;/p&gt;
&lt;p&gt;In the meantime, I plan to maintain pyrepl-hacks.
As Python 3.15 is developed, I&amp;rsquo;ll make sure it continues to work.
And I may add more useful commands if I think of any.&lt;/p&gt;
&lt;p&gt;If you hack your own REPL, I&amp;rsquo;d love to hear what modifications you come up with.
And if you have thoughts on how to improve pyrepl-hacks, please open an issue or get in touch.&lt;/p&gt;
&lt;p&gt;Also, &lt;strong&gt;if you use Windows&lt;/strong&gt;, &lt;a href="https://github.com/treyhunner/pyrepl-hacks/issues/1"&gt;please help me&lt;/a&gt; confirm &lt;strong&gt;whether certain keys work on Windows&lt;/strong&gt;. Thanks!&lt;/p&gt;
&lt;p&gt;Contributions and ideas welcome!&lt;/p&gt;</content><category term="python"/><category term="repl"/></entry><entry><title>Customizing your Python REPL's color scheme (Python 3.14+)</title><link href="https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/" rel="alternate"/><published>2025-09-04T14:00:00-07:00</published><updated>2025-09-04T14:00:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme</id><content type="html">&lt;p&gt;Did you know that Python 3.14 will include &lt;a href="https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-pyrepl-highlighting"&gt;syntax highlighting&lt;/a&gt; in the REPL?&lt;/p&gt;
&lt;p&gt;Python 3.14 is due to be &lt;a href="https://peps.python.org/pep-0745/"&gt;officially released&lt;/a&gt; in about a month.
I recommended tweaking your Python setup now so you&amp;rsquo;ll have your ideal color scheme on release day.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Python 3.14 REPL with syntax highlighting using custom color scheme" src="/images/python3.14-repl-syntax-highlighting.png"&gt;&lt;/p&gt;
&lt;p&gt;But&amp;hellip; what if the default syntax colors don&amp;rsquo;t match the colors that your text editor uses?&lt;/p&gt;
&lt;p&gt;Well, fortunately you can customize your color scheme!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: I am recommending using an undocumented private module (it has an &lt;code&gt;_&lt;/code&gt;-prefixed name) which may change in future Python versions.
Do not use this module in production code.&lt;/p&gt;
&lt;h2&gt;Installing Python 3.14&lt;/h2&gt;
&lt;p&gt;Don&amp;rsquo;t have Python 3.14 installed yet?&lt;/p&gt;
&lt;p&gt;If you have &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; installed, you can run this command to launch Python 3.14:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--python&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.14&lt;span class="w"&gt; &lt;/span&gt;python
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That will automatically install 3.14 (if you don&amp;rsquo;t have it yet) and run it.&lt;/p&gt;
&lt;h2&gt;Setting a theme&lt;/h2&gt;
&lt;p&gt;I have my terminal colors set to the Solarized Light color palette and I have Vim use a Solarized Light color scheme as well.&lt;/p&gt;
&lt;p&gt;The REPL doesn&amp;rsquo;t &lt;em&gt;quite&lt;/em&gt; match my text editor by default:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Python 3.14 REPL with default syntax highlighting" src="/images/python3.14-repl-default-color-scheme.png"&gt;&lt;/p&gt;
&lt;p&gt;The numbers, comments, strings, and keywords are all different colors than my text editor.&lt;/p&gt;
&lt;p&gt;This code makes the Python REPL use &lt;em&gt;nearly&lt;/em&gt; the same syntax highlighting as my text editor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_colorize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;set_theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Syntax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ANSIColors&lt;/span&gt;

&lt;span class="n"&gt;set_theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;syntax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Syntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;builtin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;soft_keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOLD_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Check it out:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Python 3.14 REPL with syntax highlighting using custom color scheme" src="/images/python3.14-repl-syntax-highlighting.png"&gt;&lt;/p&gt;
&lt;p&gt;Neat, right?&lt;/p&gt;
&lt;p&gt;But&amp;hellip; I want this to be enabled by default!&lt;/p&gt;
&lt;h2&gt;Using a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file&lt;/h2&gt;
&lt;p&gt;You can use a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file to run code every time a new Python process starts.&lt;/p&gt;
&lt;p&gt;If Python sees a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable when it starts up, it will open that file and evaluate the code within it.&lt;/p&gt;
&lt;p&gt;I have this in my &lt;code&gt;~/.zshrc&lt;/code&gt; file to set the &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable to &lt;code&gt;~/.startup.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Setup python-launcher to use startup file&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONSTARTUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.startup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In my &lt;code&gt;~/.startup.py&lt;/code&gt; file, I have this code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Everything&amp;#39;s in a function to avoid polluting the global scope.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_colorize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;set_theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Syntax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ANSIColors&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Python 3.13 and below&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Define Solarized Light colors&lt;/span&gt;
        &lt;span class="n"&gt;solarized_light_theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default_theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;syntax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Syntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;builtin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;soft_keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOLD_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ANSIColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTENSE_GREEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;set_theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;solarized_light_theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# _main avoids name collision, in case python -i is used&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;_main&lt;/span&gt;  &lt;span class="c1"&gt;# Delete _main from global scope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I put all relevant code within a &lt;code&gt;_main&lt;/code&gt; function so that the variables I set don&amp;rsquo;t remain in the global scope of the Python REPL (they will by default)&lt;/li&gt;
&lt;li&gt;I call the &lt;code&gt;_main&lt;/code&gt; function and then delete the function afterward, again so the &lt;code&gt;_main&lt;/code&gt; variable doesn&amp;rsquo;t stay floating around in my REPL&lt;/li&gt;
&lt;li&gt;I use &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;else&lt;/code&gt; to ensure errors don&amp;rsquo;t occur on Python 3.13 and below&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also note that the syntax highlighting in the new REPL is &lt;a href="https://github.com/python/cpython/issues/134953"&gt;not as fine-grained&lt;/a&gt; as many other syntax highlighting tools.
I suspect that it may become a bit more granular over time, &lt;strong&gt;which may break the above code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_colorize&lt;/code&gt; module is currently an internal implementation detail and is deliberately undocumented.
Its API may change at any time, so &lt;strong&gt;the above code may break in Python 3.15&lt;/strong&gt;.
If that happens, I&amp;rsquo;ll just update my &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file at that point.&lt;/p&gt;
&lt;h2&gt;Packaging themes&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve stuck all of the above code in a &lt;code&gt;~/.startup.py&lt;/code&gt; file and I set the &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable on my system to point to this file.&lt;/p&gt;
&lt;p&gt;Instead of manually updating a startup file, is there any way to make these themes &lt;em&gt;installable&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;Well, if a &lt;code&gt;.pth&lt;/code&gt; file is included in Python&amp;rsquo;s &lt;code&gt;site-packages&lt;/code&gt; directory, that file (which must be a single line) will be run whenever Python starts up.
In theory, a package could use such a file to import a module and then call a function that would set the color scheme for the REPL.
My &lt;a href="https://github.com/treyhunner/dramatic"&gt;dramatic&lt;/a&gt; package uses (&lt;em&gt;cough&lt;/em&gt; abuses &lt;em&gt;cough&lt;/em&gt;) &lt;code&gt;.pth&lt;/code&gt; files in this way.&lt;/p&gt;
&lt;p&gt;This sounds like a somewhat bad idea, but maybe not a &lt;em&gt;horrible&lt;/em&gt; idea.&lt;/p&gt;
&lt;p&gt;If you do this, let me know.&lt;/p&gt;
&lt;h2&gt;What&amp;rsquo;s your theme?&lt;/h2&gt;
&lt;p&gt;Have you played with setting a theme in your own Python REPL?&lt;/p&gt;
&lt;p&gt;What theme are you using?&lt;/p&gt;</content><category term="python"/><category term="repl"/></entry><entry><title>My PyCon US 2025 recap</title><link href="https://treyhunner.com/2025/06/pycon-us-2025-recap/" rel="alternate"/><published>2025-06-11T16:45:00-07:00</published><updated>2025-06-11T16:45:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/06/pycon-us-2025-recap</id><content type="html">&lt;p&gt;I&amp;rsquo;m pretty much fully back to normal life after PyCon US 2025.&lt;/p&gt;
&lt;p&gt;I started writing this post shortly after PyCon, got side-tracked, and now I&amp;rsquo;m finally publishing it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My very quick recap&lt;/strong&gt;: I spent a ton of time at PyCon chatting with folks and I &lt;em&gt;really&lt;/em&gt; enjoyed it.
As Ned wrote, &lt;a href="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html"&gt;it&amp;rsquo;s like summer camp&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;My pre-conference (Monday and Tuesday)&lt;/h2&gt;
&lt;p&gt;I flew in a couple days early (Monday May 12) because Breeze airlines had a non-stop flight from San Diego to Pittsburgh that only ran on Monday or Friday.
I listened to &lt;a href="http://robertdputnam.com/the-upswing/"&gt;The Upswing&lt;/a&gt; as I flew in and wondered &lt;a href="https://mastodon.social/@treyhunner/114496799621377766"&gt;how we can make our community less individualistic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On Tuesday, I went on &lt;a href="https://mastodon.social/@treyhunner/114495825740774796"&gt;a walking tour&lt;/a&gt; of downtown Pittsburgh.
Then I ate Indian food for lunch at Sree&amp;rsquo;s, which I chose because I saw they had great vegan lunch options and weren&amp;rsquo;t open for dinner.
I was eating vegan during PyCon US (as I &lt;em&gt;mostly&lt;/em&gt; do at home) and I remembered to do &lt;a href="https://mastodon.social/@treyhunner/114506541091205849"&gt;some research on vegan dinner restaurants in the area&lt;/a&gt; this year.&lt;/p&gt;
&lt;p&gt;On Tuesday night, as CPython core devs started showing up in the Westin lobby, I went on a walk with Anthony Shaw.
Anthony asked whether I was up for &amp;ldquo;an adventure&amp;rdquo; and I said yes without asking questions.
We walked across the river to Randyland (a.k.a. &amp;ldquo;the mattress factory&amp;rdquo;).
It was fun, interesting, and quite odd.
We were the only ones there and had plenty of time to look around at all the interesting items and art pieces that Randy had collected.&lt;/p&gt;
&lt;p&gt;Anthony and I did dinner at Condado&amp;rsquo;s and ran into more conference friends at the bar (Kattni, Rose, and Kojo).&lt;/p&gt;
&lt;h2&gt;My tutorial on decorators (Wednesday)&lt;/h2&gt;
&lt;p&gt;Wednesday was my tutorial day.&lt;/p&gt;
&lt;p&gt;Attendees used sticky notes during the exercise sections to note when they needed help.
I wrote about this system in my &lt;a href="https://treyhunner.com/2025/05/how-to-give-a-great-pycon-tutorial/"&gt;blog post of tips for giving a good PyCon tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The tutorial went well and the attendees seemed to enjoy it.&lt;/p&gt;
&lt;h2&gt;The newcomer&amp;rsquo;s orientation &amp;amp; education summit (Thursday)&lt;/h2&gt;
&lt;p&gt;Just before the opening reception on Thursday evening, Kojo, Sumana, and I ran the &lt;a href="https://mastodon.social/@pycon@fosstodon.org/114512798321665686"&gt;newcomer&amp;rsquo;s orientation&lt;/a&gt;.
Figuring out what exactly we want to say to help orient folks to PyCon is always a bit challenging.
A first-time PyCon attendee gave me some ideas for how we could do it even better next year.
I&amp;rsquo;ve taken notes and will revisit them later.&lt;/p&gt;
&lt;p&gt;I also attended a few talks during education summit earlier in the day.
Reuven Lerner gave a talk on how he recommends his students use LLMs and I was pleased to hear many suggestions that are closely aligned with what I recommend as well as a few insights I hadn&amp;rsquo;t heard before.&lt;/p&gt;
&lt;h2&gt;My lightning talk (Friday)&lt;/h2&gt;
&lt;p&gt;I gave a lightning talk on Friday evening (the first talk day).
I called it &lt;a href="https://youtu.be/Uuhu-F05A7k?feature=shared&amp;amp;t=3174"&gt;Systems, gates, and rage&lt;/a&gt;.
It seemed to go over well.
Folks occasionally told me throughout the conference that they enjoyed it.
I won&amp;rsquo;t spoil the topic of my lightning talk (you&amp;rsquo;ll need to watch it) but it&amp;rsquo;s a topic that I&amp;rsquo;d been thinking about for a few months.&lt;/p&gt;
&lt;p&gt;Rodrigo gave a &lt;a href="https://mastodon.social/@treyhunner/114519939925398070"&gt;meta lightning talk&lt;/a&gt; as the first talk of the first day.
I gave a meta lightning talk last year in the same slot.
I hope this becomes a tradition, where the first lightning talk is a talk about someone explaining how to give a lightning talk.&lt;/p&gt;
&lt;h2&gt;The keynotes&lt;/h2&gt;
&lt;p&gt;I really enjoyed the keynotes this year.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/ydVmzg_SJLw?feature=shared"&gt;Cory Doctorow&amp;rsquo;s opening keynote&lt;/a&gt; was &lt;a href="https://mastodon.social/@treyhunner/114519939925398070"&gt;&lt;em&gt;really&lt;/em&gt; thought-provoking&lt;/a&gt; and &lt;a href="https://youtu.be/Bglsof9b23k?feature=shared"&gt;Lynn Root&amp;rsquo;s keynote&lt;/a&gt; was on the importance &lt;a href="https://mastodon.social/@treyhunner/114519939925398070"&gt;of &amp;ldquo;play&amp;rdquo;&lt;/a&gt;, which is a topic I&amp;rsquo;ve written about before to my &lt;a href="https://pym.dev/newsletter"&gt;mailing list&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qog-dGVhSBI?feature=shared"&gt;The Marshall project&lt;/a&gt; keynote and &lt;a href="https://youtu.be/3UOLpTA7pRI?feature=shared"&gt;The Carpentries&lt;/a&gt; keynote were also great, though I missed portions of each (beginning of one and end of another) and ended up watching the full videos online after the conference.&lt;/p&gt;
&lt;h2&gt;The hallway track&lt;/h2&gt;
&lt;p&gt;The &amp;ldquo;hallway track&amp;rdquo; is the way many PyCon attendees describe &lt;strong&gt;the discussions that happen organically in the hallway&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;These discussions can happen at any time, including breaks, breakfast, lunch, and even during talks. I missed at least 3 talks this year because I was having a great discussion in the hallway and time got away from me.&lt;/p&gt;
&lt;p&gt;PyCon&amp;rsquo;s venue in Pittsburgh included a great &lt;a href="https://mastodon.social/@treyhunner/114512971414412295"&gt;rooftop&lt;/a&gt; so the &lt;strong&gt;rooftop track&lt;/strong&gt; / &lt;strong&gt;garden track&lt;/strong&gt; was a lovely spin in the hallway track this year.&lt;/p&gt;
&lt;h2&gt;The open spaces&lt;/h2&gt;
&lt;p&gt;The open spaces are a very important part of PyCon for me.&lt;/p&gt;
&lt;p&gt;Like the hallway track, the open spaces are mostly (usually) about having a conversation.
Unlike the hallway track, the open spaces have a &lt;strong&gt;set topic&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I attended open spaces (and &lt;a href="https://mastodon.social/@treyhunner/114526876252905533"&gt;ran my own&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Education and Outreach Working Group Open Space&lt;/strong&gt;: I&amp;rsquo;m part of this newly revived work group and we held this to hear topics that community members are interested in seeing the PSF focus on more deliberately&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;t-strings: Let&amp;rsquo;s build powerful templating together&lt;/strong&gt;: I am interested in the new t-string feature in Python 3.14 and wanted to listen to this conversation (I contributed &lt;em&gt;a little bit&lt;/em&gt; too)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Computer Assisted Reporting and Investigative journalism&lt;/strong&gt;: I was curious to hear this conversation, which included journalists, government employees who respond to information requests, and many others&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python Skill-Building&lt;/strong&gt;: I ran this open space on the roof during lunch time to chat about different Python skill-building services, including my own (&lt;a href="https://pym.dev"&gt;Python Morsels&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask a Friendly Meat-Loving Vegan&lt;/strong&gt;: I also ran this open space, inspired by a conversation I had the night before in a hotel lobby about animal farming, veganism, nutrition, and other related topics I&amp;rsquo;ve thought/read a lot about&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I enjoyed meeting folks with similar interests and having fun and productive conversations (or at least listening to interesting conversations) in those open spaces.&lt;/p&gt;
&lt;h2&gt;Dinners, games, and hotel lobby conversations&lt;/h2&gt;
&lt;p&gt;I ate at Bae Bae&amp;rsquo;s, Rosewater Grill, and APTEKA during the conference.
I also ate at Condado&amp;rsquo;s at least 3 nights.&lt;/p&gt;
&lt;p&gt;APTEKA was a fun trek for &lt;a href="https://mastodon.social/@pythonbynight@fosstodon.org/114525581821316666"&gt;vegan PierogiCon&lt;/a&gt; with mostly non-vegan folks.
I&amp;rsquo;m sure the pierogis weren&amp;rsquo;t authentic, but we all loved the food.&lt;/p&gt;
&lt;p&gt;After dinner every night I went to the Westin lobby and either played &lt;a href="https://treyhunner.com/2015/06/cabo-card-game/"&gt;the Cabo card game&lt;/a&gt; or chatted with folks (&lt;a href="https://mastodon.social/@treyhunner/114509076754402356"&gt;1&lt;/a&gt;, &lt;a href="https://mastodon.social/@treyhunner/114514682814457694"&gt;2&lt;/a&gt;, &lt;a href="https://mastodon.social/@treyhunner/114525878624139679"&gt;3&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;On missing talks&lt;/h2&gt;
&lt;p&gt;The keynotes and lightning talks are usually my favorite parts of PyCon.
I tried to watch as many live keynotes and lightning talk sessions as I could this year.
The morning lightning talks were hard to make it to because I kept sleeping in late enough that I missed most of them.&lt;/p&gt;
&lt;p&gt;Ultimately, I watched &lt;em&gt;very few&lt;/em&gt; live talks.
I missed talks due to sleeping in, attending open spaces, visiting booths in the exhibit hall, getting lost in conversations in the hallway, and mid-day taking naps (in my hotel room, not during talks!).&lt;/p&gt;
&lt;p&gt;Hynek has &lt;a href="https://hynek.me/articles/hallway-track/"&gt;written about the downside of the hallway track and the importance of attending talks&lt;/a&gt;.
I sympathize with Hynek&amp;rsquo;s argument that the hallway track is a trade off and there are downsides to missing talks, for both attendees and speakers.
I am grateful that folks give talks and I want to support folks who do give talks, and yet, I often find myself attending few talks.&lt;/p&gt;
&lt;p&gt;Ironically, the one talk I ended up taking a nap through (again, in my hotel room, not &lt;em&gt;in the talk&lt;/em&gt;) was Hynek&amp;rsquo;s.
My brain was fried by the time of his talk and I&amp;rsquo;m glad I was able to watch it online the week after PyCon.
He apparently had a completely packed room as it was!&lt;/p&gt;
&lt;p&gt;I did make sure to show up to a talk by a conference friend who I wanted to support (Michael Dahlberg&amp;rsquo;s &lt;a href="https://youtu.be/LseCKvrp6og?feature=shared"&gt;talk on honeybee swarms&lt;/a&gt;).
I have also binge-watched a dozen or so talks online that I had planned to attend but missed during PyCon.&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://www.youtube.com/@PyConUS/videos"&gt;watch all talks online now&lt;/a&gt; (at least I &lt;em&gt;think&lt;/em&gt; they&amp;rsquo;re all up now?).&lt;/p&gt;
&lt;h2&gt;Attend a Python event&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m looking forward to PyCon coming to Long Beach next year.
I&amp;rsquo;m in San Diego and it will be nice to &lt;em&gt;not fly&lt;/em&gt; to a PyCon for once!&lt;/p&gt;
&lt;p&gt;I highly recommend attending local Python meetups, attending regional Python conferences, and/or attending PyCon US.
Most in-person Python community events are &lt;em&gt;pretty lovely&lt;/em&gt;.
The Python community generally goes out of its way to be more welcoming than most tech events.&lt;/p&gt;
&lt;p&gt;See you in Long Beach!&lt;/p&gt;</content><category term="pycon"/><category term="python"/></entry><entry><title>Which social network are we using for PyCon US this year?</title><link href="https://treyhunner.com/2025/04/which-social-network-are-we-using-for-pycon/" rel="alternate"/><published>2025-04-18T11:45:00-07:00</published><updated>2025-04-18T11:45:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2025/04/which-social-network-are-we-using-for-pycon</id><content type="html">&lt;p&gt;Last year I updated my &lt;a href="https://treyhunner.com/2018/04/how-to-make-the-most-of-your-first-pycon/"&gt;having a great first PyCon&lt;/a&gt; post to note that Mastodon would likely be more popular than Twitter at PyCon.&lt;/p&gt;
&lt;p&gt;My guess was correct.
During PyCon US 2024, &lt;a href="https://mastodon.social/@treyhunner/112453920848761679"&gt;Mastodon overtook Twitter&lt;/a&gt; for the most posts on the &lt;code&gt;#PyConUS&lt;/code&gt; hashtag.&lt;/p&gt;
&lt;p&gt;In the fall of 2024, Bluesky really took off.
It currently seems like Bluesky is now a bit more popular than Mastodon for Python posts in general&amp;hellip; but that doesn&amp;rsquo;t necessarily mean it will be the most popular social media network during the conference.&lt;/p&gt;
&lt;p&gt;This year I&amp;rsquo;m guessing that Mastodon will still be the most popular network for &lt;code&gt;#PyConUS&lt;/code&gt; posts, though I wouldn&amp;rsquo;t be very surprised if Bluesky took the lead instead.&lt;/p&gt;
&lt;h2&gt;What do the social networks think?&lt;/h2&gt;
&lt;p&gt;I decided to pose this question to the various social networks via a poll. I made sure to share these polls using both the &lt;code&gt;#Python&lt;/code&gt; and &lt;code&gt;#PyConUS&lt;/code&gt; hashtags for visibility&amp;rsquo;s sake.&lt;/p&gt;
&lt;p&gt;The results?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/posts/treyhunner_pyconus-python-activity-7313960284581216256-DdzA/"&gt;LinkedIn poll&lt;/a&gt;: LinkedIn isn&amp;rsquo;t sure whether Twitter is the new Twitter&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/treyhunner/status/1908187705965703323"&gt;Twitter poll&lt;/a&gt; Twitter (mostly) knows that Bluesky is the new Twitter&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/trey.io/post/3llyswsgkut2h"&gt;Bluesky poll&lt;/a&gt; Bluesky knows the really nerdy tech folks hang out on Mastodon&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mastodon.social/@treyhunner/114280595068705311"&gt;Mastodon poll&lt;/a&gt; Mastodon loves itself above all else&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, LinkedIn leans more toward Bluesky being the leader than Mastodon, Twitter leans toward Bluesky, but Bluesky and Mastodon both lean toward Mastodon being the leader during PyCon.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t mention Threads or other platforms because they didn&amp;rsquo;t seem like real contenders given where the most active PyCon-attending Python-posting folks seem to hang out in 2025.&lt;/p&gt;
&lt;h2&gt;The actual results&lt;/h2&gt;
&lt;p&gt;Here are the actual results of the polls.&lt;/p&gt;
&lt;h3&gt;LinkedIn&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/posts/treyhunner_pyconus-python-activity-7313960284581216256-DdzA/"&gt;&lt;img alt="LinkedIn poll results" src="/images/pycon-poll-linkedin-2025.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Twitter&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://x.com/treyhunner/status/1908187705965703323"&gt;&lt;img alt="Twitter poll results" src="/images/pycon-poll-twitter-2025.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Bluesky&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://bsky.app/profile/trey.io/post/3llyswsgkut2h"&gt;&lt;img alt="Bluesky poll results" src="/images/pycon-poll-bluesky-2025.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Mastodon&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://mastodon.social/@treyhunner/114280595068705311"&gt;&lt;img alt="Mastodon poll results" src="/images/pycon-poll-mastodon-2025.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;We&amp;rsquo;re still fragmented&lt;/h2&gt;
&lt;p&gt;Unfortunately, regardless of which network is the leader this year during PyCon US, we&amp;rsquo;re still going to be much more fragmented than we used to be. Twitter &lt;em&gt;was&lt;/em&gt; the clear leader years ago and it very clearly isn&amp;rsquo;t anymore&amp;hellip; at least not for PyCon US conference chatter.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be checking Mastodon and Bluesky and will post on &lt;em&gt;at least&lt;/em&gt; Mastodon and possibly also Bluesky. I hope that other folks will also use one of these 2 social networks to organize dinners and gatherings! 🤞&lt;/p&gt;
&lt;p&gt;Feel free to follow me &lt;a href="https://mastodon.social/@treyhunner"&gt;on Mastodon&lt;/a&gt; and &lt;a href="https://mastodon.social/@treyhunner"&gt;Bluesky&lt;/a&gt; during the conference.&lt;/p&gt;
&lt;p&gt;Also let me know if you&amp;rsquo;d like to join my &lt;a href="https://bsky.app/profile/trey.io/post/3ln45jpzmik2c"&gt;PyCon US starter pack&lt;/a&gt; on Bluesky.
Lists on Mastodon require following and I prefer not to follow everyone I meet at PyCon so, unfortunately, I probably won&amp;rsquo;t have a Mastodon equivalent of this. 😢&lt;/p&gt;
&lt;p&gt;I recommend checking the &lt;code&gt;#PyConUS&lt;/code&gt; hashtag on both networks as well.
Note that you can &lt;em&gt;subscribe&lt;/em&gt; to hashtags on Mastodon which is pretty neat!&lt;/p&gt;
&lt;h2&gt;See you at PyCon!&lt;/h2&gt;
&lt;p&gt;If this will be your first PyCon US, I recommend signing up for both Mastodon and Bluesky and checking the &lt;code&gt;#PyConUS&lt;/code&gt; hashtag during the conference.&lt;/p&gt;
&lt;p&gt;Also, be sure to see my &lt;a href="https://treyhunner.com/2018/04/how-to-make-the-most-of-your-first-pycon/"&gt;post on having a great first PyCon&lt;/a&gt; and &lt;a href="https://mastodon.social/@treyhunner/112448459788776426"&gt;see this additional tips&lt;/a&gt;.&lt;/p&gt;</content><category term="python"/><category term="pycon"/></entry><entry><title>Lazy self-installing Python scripts with uv</title><link href="https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/" rel="alternate"/><published>2024-12-09T11:15:10-08:00</published><updated>2024-12-09T11:15:10-08:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv</id><content type="html">&lt;p&gt;I frequently find myself writing my own short command-line scripts in Python that help me with day-to-day tasks.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s &lt;em&gt;so&lt;/em&gt; easy to throw together a single-file Python command-line script and throw it in my &lt;code&gt;~/bin&lt;/code&gt; directory!&lt;/p&gt;
&lt;p&gt;Well&amp;hellip; it&amp;rsquo;s easy, &lt;em&gt;unless&lt;/em&gt; the script requires anything outside of the Python standard library.&lt;/p&gt;
&lt;p&gt;Recently I&amp;rsquo;ve started using uv and my &lt;em&gt;primary&lt;/em&gt; for use for it has been fixing Python&amp;rsquo;s &amp;ldquo;just manage the dependencies automatically&amp;rdquo; problem.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll share how I&amp;rsquo;ve been using uv&amp;hellip; first first let&amp;rsquo;s look at the problem.&lt;/p&gt;
&lt;h2&gt;A script without dependencies&lt;/h2&gt;
&lt;p&gt;If I have a Python script that I want to be easily usable from anywhere on my system, I typically follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add an appropriate shebang line above the first line in the file (e.g. &lt;code&gt;#!/usr/bin/env python3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Set an executable bit on the file (&lt;code&gt;chmod a+x my_script.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Place the script in a directory that&amp;rsquo;s in my shell&amp;rsquo;s &lt;code&gt;PATH&lt;/code&gt; variable (e.g. &lt;code&gt;cp my_script.py ~/bin/my_script&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, here&amp;rsquo;s a script I use to print out 80 zeroes (or a specific number of zeroes) to check whether my terminal&amp;rsquo;s font size is large enough when I&amp;rsquo;m teaching:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This file lives at &lt;code&gt;/home/trey/bin/0&lt;/code&gt; so I can run the command &lt;code&gt;0&lt;/code&gt; from my system prompt to see 80 &lt;code&gt;0&lt;/code&gt; characters printed in my terminal.&lt;/p&gt;
&lt;p&gt;This works great!
But this script doesn&amp;rsquo;t have any dependencies.&lt;/p&gt;
&lt;h2&gt;The problem: a script with dependencies&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a Python script that normalizes the audio of a given video file and writes a new audio-normalized version of the video to a new file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Normalize audio in input video file.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;argparse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ArgumentParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ffmpeg_normalize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FFmpegNormalize&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;normalize_audio_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_normalized_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Return audio-normalized video file saved in the given directory.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;ffmpeg_normalize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FFmpegNormalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_codec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;aac&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_bitrate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;192k&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_level&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ffmpeg_normalize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_media_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;audio_normalized_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ffmpeg_normalize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_normalization&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;video_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;output_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;normalize_audio_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This script depends on the &lt;a href="https://github.com/slhck/ffmpeg-normalize"&gt;ffmpeg-normalize&lt;/a&gt; Python package and the &lt;a href="https://ffmpeg.org"&gt;ffmpeg&lt;/a&gt; utility.
I already have &lt;code&gt;ffmpeg&lt;/code&gt; installed, but I prefer &lt;em&gt;not&lt;/em&gt; to globally install Python packages.
I install all Python packages within virtual environments and I install global Python scripts using &lt;a href="https://pipx.pypa.io"&gt;pipx&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point I &lt;em&gt;could&lt;/em&gt; choose to either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a virtual environment, install &lt;code&gt;ffmpeg-normalize&lt;/code&gt; in it, and put a shebang line referencing that virtual environment&amp;rsquo;s Python binary at the top of my script file&lt;/li&gt;
&lt;li&gt;Turn my script into a &lt;code&gt;pip&lt;/code&gt;-installable Python package with a &lt;code&gt;pyproject.toml&lt;/code&gt; that lists &lt;code&gt;ffmpeg-normalize&lt;/code&gt; as a dependency and use &lt;code&gt;pipx&lt;/code&gt; to install it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That first solution requires me to keep track of virtual environments that exist for specific scripts to work.
That sounds painful.&lt;/p&gt;
&lt;p&gt;The second solution involves making a Python package and then upgrading that Python package whenever I need to make a change to this script.
That&amp;rsquo;s definitely going to be painful.&lt;/p&gt;
&lt;h2&gt;The solution: let uv handle it&lt;/h2&gt;
&lt;p&gt;A few months ago, my friend &lt;a href="https://micro.webology.dev"&gt;Jeff Triplett&lt;/a&gt; showed me that &lt;code&gt;uv&lt;/code&gt; can work within a shebang line and can read a special comment at the top of a Python file that tells uv which Python version to run a script with and which dependencies it needs.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a shebang line that would work for the above script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env -S uv run --script&lt;/span&gt;
&lt;span class="c1"&gt;# /// script&lt;/span&gt;
&lt;span class="c1"&gt;# requires-python = &amp;quot;&amp;gt;=3.12&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# dependencies = [&lt;/span&gt;
&lt;span class="c1"&gt;#     &amp;quot;ffmpeg-normalize&amp;quot;,&lt;/span&gt;
&lt;span class="c1"&gt;# ]&lt;/span&gt;
&lt;span class="c1"&gt;# ///&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That tells uv that this script should be run on Python 3.12 and that it depends on the &lt;code&gt;ffmpeg-normalize&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;Neat&amp;hellip; but what does that do?&lt;/p&gt;
&lt;p&gt;Well, the first time this script is run, uv will create a virtual environment for it, install &lt;code&gt;ffmpeg-normalize&lt;/code&gt; into that venv, and then run the script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;normalize
Reading&lt;span class="w"&gt; &lt;/span&gt;inline&lt;span class="w"&gt; &lt;/span&gt;script&lt;span class="w"&gt; &lt;/span&gt;metadata&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;/home/trey/bin/normalize&lt;span class="sb"&gt;`&lt;/span&gt;
Installed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;packages&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;5ms
usage:&lt;span class="w"&gt; &lt;/span&gt;normalize&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;-h&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;video_file&lt;span class="w"&gt; &lt;/span&gt;output_file
normalize:&lt;span class="w"&gt; &lt;/span&gt;error:&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;arguments&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;required:&lt;span class="w"&gt; &lt;/span&gt;video_file,&lt;span class="w"&gt; &lt;/span&gt;output_file
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every time the script is run after that, uv finds and reuses the same virtual environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;normalize
Reading&lt;span class="w"&gt; &lt;/span&gt;inline&lt;span class="w"&gt; &lt;/span&gt;script&lt;span class="w"&gt; &lt;/span&gt;metadata&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;/home/trey/bin/normalize&lt;span class="sb"&gt;`&lt;/span&gt;
usage:&lt;span class="w"&gt; &lt;/span&gt;normalize&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;-h&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;video_file&lt;span class="w"&gt; &lt;/span&gt;output_file
normalize:&lt;span class="w"&gt; &lt;/span&gt;error:&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;arguments&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;required:&lt;span class="w"&gt; &lt;/span&gt;video_file,&lt;span class="w"&gt; &lt;/span&gt;output_file
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each time uv runs the script, it quickly checks that all listed dependencies are properly installed with their correct versions.&lt;/p&gt;
&lt;p&gt;Another script I use this for is &lt;a href="https://github.com/treyhunner/dotfiles/blob/main/bin/caption"&gt;caption&lt;/a&gt;, which uses whisper (via the Open AI API) to quickly caption my screencasts just after I record and edit them.
The caption quality very rarely need more than a very minor edit or two (for my personal accent of English at least) even for technical like &amp;ldquo;dunder method&amp;rdquo; and via the API the captions generate very quickly.&lt;/p&gt;
&lt;p&gt;See the &lt;a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/"&gt;inline script metadata&lt;/a&gt; page of the Python packaging users guide for more details on that format that uv is using (honestly I always just copy-paste an example myself).&lt;/p&gt;
&lt;h2&gt;uv everywhere?&lt;/h2&gt;
&lt;p&gt;I haven&amp;rsquo;t yet fully embraced uv everywhere.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t manage my Python projects with uv, though I do use it to create new virtual environments (with &lt;code&gt;--seed&lt;/code&gt; to ensure the &lt;code&gt;pip&lt;/code&gt; command is available) as a &lt;a href="https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv/"&gt;virtualenvwrapper replacement, along with direnv&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have also started using &lt;a href="https://docs.astral.sh/uv/concepts/tools/"&gt;uv tool&lt;/a&gt; as a &lt;a href="https://pipx.pypa.io"&gt;pipx&lt;/a&gt; replacement and I&amp;rsquo;ve considered replacing &lt;a href="https://pipx.pypa.io/stable/"&gt;pyenv&lt;/a&gt; with uv.&lt;/p&gt;
&lt;h2&gt;uv instead of pipx&lt;/h2&gt;
&lt;p&gt;When I want to install a command-line tool that happens to be Python powered, I used to do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pipx&lt;span class="w"&gt; &lt;/span&gt;countdown-cli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now I do this instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;countdown-cli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Either way, I end up with a &lt;code&gt;countdown&lt;/code&gt; script in my &lt;code&gt;PATH&lt;/code&gt; that automatically uses its own separate virtual environment for its dependencies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;countdown&lt;span class="w"&gt; &lt;/span&gt;--help
Usage:&lt;span class="w"&gt; &lt;/span&gt;countdown&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;OPTIONS&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;DURATION

&lt;span class="w"&gt;  &lt;/span&gt;Countdown&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;given&lt;span class="w"&gt; &lt;/span&gt;duration&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.

&lt;span class="w"&gt;  &lt;/span&gt;DURATION&lt;span class="w"&gt; &lt;/span&gt;should&lt;span class="w"&gt; &lt;/span&gt;be&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;number&lt;span class="w"&gt; &lt;/span&gt;followed&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;m&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;minutes&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;seconds.

&lt;span class="w"&gt;  &lt;/span&gt;Examples&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;DURATION:

&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;5m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;minutes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;45s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;45&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;seconds&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;2m30s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;minutes&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;seconds&lt;span class="o"&gt;)&lt;/span&gt;

Options:
&lt;span class="w"&gt;  &lt;/span&gt;--version&lt;span class="w"&gt;  &lt;/span&gt;Show&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;exit.
&lt;span class="w"&gt;  &lt;/span&gt;--help&lt;span class="w"&gt;     &lt;/span&gt;Show&lt;span class="w"&gt; &lt;/span&gt;this&lt;span class="w"&gt; &lt;/span&gt;message&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;exit.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;uv instead of pyenv&lt;/h2&gt;
&lt;p&gt;For years, I&amp;rsquo;ve used pyenv to manage multiple versions of Python on my machine.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pyenv&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.13.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now I could do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--preview&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.13.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or I could make a &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt; file containing this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;preview = true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then run the same thing without the &lt;code&gt;--preview&lt;/code&gt; flag:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.13.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This puts a &lt;code&gt;python3.13&lt;/code&gt; binary in my &lt;code&gt;~/.local/bin directory&lt;/code&gt;, which is on my &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why &amp;ldquo;preview&amp;rdquo;?
Well, without it uv doesn&amp;rsquo;t (&lt;a href="https://github.com/astral-sh/uv/issues/6265#issuecomment-2461107903"&gt;yet&lt;/a&gt;) place &lt;code&gt;python3.13&lt;/code&gt; in my &lt;code&gt;PATH&lt;/code&gt; by default, as this feature is currently in testing/development.&lt;/p&gt;
&lt;h2&gt;Update: uvrs for managing uv scripts&lt;/h2&gt;
&lt;p&gt;Since writing this post, I created &lt;a href="https://github.com/treyhunner/uvrs"&gt;uvrs&lt;/a&gt;, a tool for managing uv scripts.&lt;/p&gt;
&lt;p&gt;With uvrs, you can use &lt;code&gt;uvrs add&lt;/code&gt; and &lt;code&gt;uvrs remove&lt;/code&gt; to manage your script&amp;rsquo;s inline dependencies, and &lt;code&gt;uvrs init&lt;/code&gt; to create new scripts with the right metadata and executable permissions already set.&lt;/p&gt;
&lt;p&gt;Most importantly, uvrs adds an &lt;code&gt;exclude-newer&lt;/code&gt; timestamp to your script&amp;rsquo;s metadata, which limits dependency versions to those published before a given date.
This means your scripts won&amp;rsquo;t unexpectedly break when a dependency releases a new version.
When you&amp;rsquo;re ready to upgrade, &lt;code&gt;uvrs stamp&lt;/code&gt; updates the timestamp and upgrades your dependencies.&lt;/p&gt;
&lt;p&gt;uvrs also runs scripts with &lt;code&gt;uv run --exact --script&lt;/code&gt;, ensuring the script&amp;rsquo;s environment always matches its metadata exactly.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re using uv scripts regularly, &lt;a href="https://github.com/treyhunner/uvrs"&gt;uvrs&lt;/a&gt; makes them much easier to manage and more reproducible.&lt;/p&gt;
&lt;h2&gt;Self-installing Python scripts are the big win&lt;/h2&gt;
&lt;p&gt;I still prefer pyenv for its ability to &lt;a href="https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv/"&gt;install custom Python builds&lt;/a&gt; and I don&amp;rsquo;t have a preference between &lt;code&gt;uv tool&lt;/code&gt; and &lt;code&gt;pipx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The biggest win that I&amp;rsquo;ve experienced from uv so far is the ability to run an executable script and have any necessary dependencies install automagically.&lt;/p&gt;
&lt;p&gt;This doesn&amp;rsquo;t mean that I &lt;em&gt;never&lt;/em&gt; make Python package out of my Python scripts anymore&amp;hellip; but I do so much more rarely.
I used to create a Python package out of a script as soon as it required third-party dependencies.
Now my &amp;ldquo;do I &lt;em&gt;really&lt;/em&gt; need to turn this into a proper package&amp;rdquo; bar is set much higher.&lt;/p&gt;</content><category term="python"/><category term="uv"/></entry><entry><title>New Python Jumpstart course</title><link href="https://treyhunner.com/2024/11/new-python-jumpstart-course/" rel="alternate"/><published>2024-11-25T08:08:08-08:00</published><updated>2024-11-25T08:08:08-08:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/11/new-python-jumpstart-course</id><content type="html">&lt;p&gt;I&amp;rsquo;ve just recently launched a self-paced introduction to Python that is &lt;strong&gt;extremely hands-on&lt;/strong&gt;.
It&amp;rsquo;s called &lt;strong&gt;&lt;a href="https://pym.dev/courses/jumpstart/overview"&gt;Python Jumpstart&lt;/a&gt;&lt;/strong&gt; and it&amp;rsquo;s based on introductory Python curriculum that I have been iterating on for years.&lt;/p&gt;
&lt;h2&gt;Learn Python by writing Python code ✍&lt;/h2&gt;
&lt;p&gt;We &lt;em&gt;do not&lt;/em&gt; learn by putting information into the brain.
Our brains simply don&amp;rsquo;t retain knowledge that way.&lt;/p&gt;
&lt;p&gt;Learning happens from repeatedly attempting to retrieve information &lt;em&gt;from&lt;/em&gt; the brain.
When it comes to Python, that means &lt;strong&gt;writing Python code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So unlike most Python courses, Python Jumpstart is &lt;em&gt;not&lt;/em&gt; focused on watching videos and rewriting code seen within videos.
Instead, this new Python course is based around &lt;strong&gt;learning Python by writing Python code&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;How Python Jumpstart is structured 🔬&lt;/h2&gt;
&lt;p&gt;Python Jumpstart includes learning Python by solving &lt;strong&gt;46 short Python exercises&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Before each exercise, you&amp;rsquo;ll watch a 5 minute video explaining a new Python topic.
You&amp;rsquo;ll then attempt the exercise to put one or more Python topics into practice.&lt;/p&gt;
&lt;p&gt;You won&amp;rsquo;t solve many of the exercises on your first try and &lt;em&gt;that&amp;rsquo;s okay&lt;/em&gt;.
These exercises are designed to be revisited a few times over the course of weeks, until you&amp;rsquo;re satisfied with your solution.&lt;/p&gt;
&lt;h2&gt;This is not a sprint 📆&lt;/h2&gt;
&lt;p&gt;This structured path to Python proficiency, includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Carefully crafted exercises that build real understanding&lt;/li&gt;
&lt;li&gt;Short, detailed explanations that &lt;em&gt;won&amp;rsquo;t&lt;/em&gt; waste your time&lt;/li&gt;
&lt;li&gt;A proven teaching approach that focuses on active learning&lt;/li&gt;
&lt;li&gt;Spaced repetition to help concepts stick&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not a course for impatient or passive learners.
Learning takes &lt;strong&gt;repetitive effort&lt;/strong&gt; spaced over many days and this course is structured to embrace that fact.&lt;/p&gt;
&lt;p&gt;If you spend &lt;strong&gt;30 minutes&lt;/strong&gt; on Python Jumpstart each day, I estimate that you&amp;rsquo;ll complete this course in &lt;strong&gt;about 7 weeks&lt;/strong&gt;.
You&amp;rsquo;ll spend the large majority of that time writing Python code.&lt;/p&gt;
&lt;p&gt;This course &lt;em&gt;will&lt;/em&gt; take time, but it will be time well-spent.&lt;/p&gt;
&lt;h2&gt;Launch week special: 50% off until December 2 ⏰&lt;/h2&gt;
&lt;p&gt;Through Monday December 2, 2024, you can get lifetime access to &lt;a href="https://pym.dev/courses/jumpstart/overview"&gt;Python Jumpstart&lt;/a&gt; for &lt;strong&gt;$99&lt;/strong&gt;.
After this launch week, Python Jumpstart will be &lt;strong&gt;$199&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Ready to jumpstart your Python learning journey?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pym.dev/courses/jumpstart/overview" class="subscribe-btn form-big"&gt;Get Python Jumpstart for $99&lt;/a&gt;&lt;/p&gt;</content><category term="python"/><category term="sales"/></entry><entry><title>Python Black Friday &amp; Cyber Monday sales (2024)</title><link href="https://treyhunner.com/2024/11/python-black-friday-and-cyber-monday-sales-2024/" rel="alternate"/><published>2024-11-20T11:00:00-08:00</published><updated>2024-11-20T11:00:00-08:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/11/python-black-friday-and-cyber-monday-sales-2024</id><content type="html">&lt;p&gt;Ready for some Python skill-building sales?&lt;/p&gt;
&lt;p&gt;This is my &lt;strong&gt;&lt;a href="https://treyhunner.com/blog/categories/sales/"&gt;seventh&lt;/a&gt;&lt;/strong&gt; annual compilation of &lt;strong&gt;Python learning deals&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Lots of Python sales&lt;/h2&gt;
&lt;p&gt;Here are Python-related sales that are live right now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.pythonmorsels.com/courses/jumpstart/overview/"&gt;Python Jumpstart&lt;/a&gt;&lt;/strong&gt; with Python Morsels: &lt;strong&gt;50% off&lt;/strong&gt; my brand new Python course, an introduction to Python that&amp;rsquo;s &lt;em&gt;very&lt;/em&gt; hands-on (&lt;strong&gt;$99&lt;/strong&gt; instead of &lt;strong&gt;$199&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://talkpython.fm/black-friday"&gt;Talk Python&lt;/a&gt;&lt;/strong&gt;: the annual everything bundle includes all courses for $240&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://courses.dataschool.io/black-friday"&gt;Data School&lt;/a&gt;&lt;/strong&gt; 40% off all Kevin&amp;rsquo;s courses or get a bundle with all 5 of his courses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://store.metasnake.com/?coupon=EMAIL40"&gt;Matt Harrison&lt;/a&gt;&lt;/strong&gt;: get 25% off with GIVING25 plus every purchase will open a product for a scholarship recipient&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://lernerpython.com/"&gt;Reuven Lerner&lt;/a&gt;&lt;/strong&gt;: get 20% off &lt;a href="https://www.bambooweekly.com/bf2024"&gt;Bamboo Weekly&lt;/a&gt; or 30% off any of his Weekly Python Exercise &lt;a href="https://lernerpython.com"&gt;courses&lt;/a&gt; with code BF2024&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://courses.pythontest.com"&gt;Brian Okken&lt;/a&gt;&lt;/strong&gt;: is offering 20% off his courses with code TURKEYSALE2024&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://mathspp.gumroad.com/"&gt;Rodrigo&lt;/a&gt;&lt;/strong&gt; 50% off Rodrigo&amp;rsquo;s &lt;a href="https://mathspp.gumroad.com/l/all-books-bundle/BF24"&gt;all books bundle&lt;/a&gt; with code &lt;code&gt;BF24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.blog.pythonlibrary.org"&gt;Mike Driscoll&lt;/a&gt;&lt;/strong&gt;: 35% off Mike&amp;rsquo;s Python &lt;a href="https://driscollis.gumroad.com/"&gt;books&lt;/a&gt; and &lt;a href="https://www.teachmepython.com/"&gt;courses&lt;/a&gt; with code &lt;code&gt;BF24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://testdriven.io/bundle/flask-black-friday/?ref=blackfridaydealsdotdev"&gt;Test Driven&lt;/a&gt;&lt;/strong&gt;: get the Flask course bundle for 30% off&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://thepythoncodingplace.com/membership/"&gt;The Python Coding Place&lt;/a&gt;&lt;/strong&gt;: 40% off &lt;a href="https://thepythoncodingplace.thinkific.com/enroll/2906653?coupon=black2024"&gt;The Python Coding Book&lt;/a&gt; and 40% off a lifetime membership to &lt;a href="https://thepythoncodingplace.thinkific.com/cart/add_product/2731141?price_id=3865919&amp;amp;coupon=black2024"&gt;The Python Coding Place&lt;/a&gt; with code &lt;code&gt;black2024&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://learnbyexample.gumroad.com"&gt;Sundeep Agarwal&lt;/a&gt;&lt;/strong&gt;: ~50% off Sundeep&amp;rsquo;s &lt;a href="https://learnbyexample.gumroad.com/l/all-books/FestiveOffer"&gt;all book&lt;/a&gt; and &lt;a href="https://learnbyexample.gumroad.com/l/python-bundle/FestiveOffer"&gt;Python&lt;/a&gt; bundles with code &lt;code&gt;FestiveOffer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://benjaminb.gumroad.com/l/xjgtb/bLACK60"&gt;Benjamin Bennett Alexander&lt;/a&gt;&lt;/strong&gt;: 60% off his Mastering Python Fundamentals book with code &lt;code&gt;BLACK60&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://learning.oreilly.com/signup/?promotion_code=CYBERWEEK24"&gt;O&amp;rsquo;Reilly Media&lt;/a&gt;&lt;/strong&gt;: 40% off the first year with code &lt;code&gt;CYBERWEEK24&lt;/code&gt; ($299 instead of $499)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://Matplotlib-journey.com"&gt;Matplotlib Journey&lt;/a&gt;&lt;/strong&gt;: a Python dataviz course that has a pre-launch promo right now (69€ instead of 149€)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nodeledge.ai/courses/python-done-right"&gt;Python Done Right&lt;/a&gt;&lt;/strong&gt;: beta sale on this course ($99 instead of the eventual price of $299)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://pragprog.com/"&gt;Pragmatic Bookshelf&lt;/a&gt;&lt;/strong&gt;: 40% off sale with code &lt;code&gt;turkeysale2024&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nostarch.com/catalog/python"&gt;No Starch&lt;/a&gt;&lt;/strong&gt;: 35% off with code &lt;code&gt;BLACKHATFRIDAY&lt;/code&gt; (Crash Course, Automate The Boring Stuff, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.manning.com/catalog#section-50"&gt;Manning&lt;/a&gt;&lt;/strong&gt; is offering 50% off if you buy two items&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Even more sales&lt;/h2&gt;
&lt;p&gt;Also see Adam Johnson&amp;rsquo;s &lt;a href="https://adamj.eu/tech/2024/11/18/django-black-friday-deals-2024/"&gt;Django-related Deals for Black Friday 2024&lt;/a&gt; for sales on Adam&amp;rsquo;s books, courses from the folks at Test Driven, Django templates, and various other Django-related deals.&lt;/p&gt;
&lt;p&gt;And for non-Python/Django Python deals, see the &lt;a href="https://github.com/trungdq88/Awesome-Black-Friday-Cyber-Monday#readme"&gt;Awesome Black Friday / Cyber Monday deals&lt;/a&gt; GitHub repository and the &lt;a href="https://blackfridaydeals.dev"&gt;BlackFridayDeals.dev&lt;/a&gt; website.&lt;/p&gt;
&lt;p&gt;If you know of another sale (or a likely sale) &lt;strong&gt;please comment below&lt;/strong&gt; or email me.&lt;/p&gt;
&lt;h2&gt;Read more about Python Jumpstart&lt;/h2&gt;
&lt;p&gt;Want to read more about my new self-paced Intro to Python course that&amp;rsquo;s on sale?
See &lt;a href="https://treyhunner.com/2024/11/new-python-jumpstart-course/"&gt;this blog post&lt;/a&gt; on why I made Python Jumpstart and how it&amp;rsquo;s structured.
Get it by Monday to &lt;strong&gt;save $100&lt;/strong&gt;.&lt;/p&gt;</content><category term="python"/><category term="sales"/></entry><entry><title>Adding keyboard shortcuts to the Python REPL</title><link href="https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/" rel="alternate"/><published>2024-10-28T07:15:00-07:00</published><updated>2024-10-28T07:15:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl</id><content type="html">&lt;p&gt;I talked about the new Python 3.13 REPL &lt;a href="https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature/"&gt;a few months ago&lt;/a&gt; and &lt;a href="https://www.pythonmorsels.com/python-313-whats-new/"&gt;after 3.13 was released&lt;/a&gt;.
I think it&amp;rsquo;s &lt;strong&gt;awesome&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to share a secret feature within the Python 3.13 REPL which I&amp;rsquo;ve been finding useful recently: &lt;strong&gt;adding custom keyboard shortcuts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This feature involves a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file, use of an unsupported Python module, and dynamically evaluating code.&lt;/p&gt;
&lt;p&gt;In short, we may be getting ourselves into trouble.
But the result is &lt;em&gt;very&lt;/em&gt; neat!&lt;/p&gt;
&lt;p&gt;Thanks to Łukasz Llanga for inspiring this post via his excellent &lt;a href="https://youtu.be/dK6HGcSb60Y?si=jWPEa8BcdYGnW9l6"&gt;EuroPython keynote talk&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The goal: keyboard shortcuts in the REPL&lt;/h2&gt;
&lt;p&gt;First, I&amp;rsquo;d like to explain the end result.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say I&amp;rsquo;m in the Python REPL on my machine and I&amp;rsquo;ve typed &lt;code&gt;numbers =&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I can now hit &lt;code&gt;Ctrl-N&lt;/code&gt; to enter a list of numbers I often use while teaching (&lt;a href="https://en.wikipedia.org/wiki/Lucas_number"&gt;Lucas numbers&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;numbers = [2, 1, 3, 4, 7, 11, 18, 29]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That saved me some typing!&lt;/p&gt;
&lt;h2&gt;Getting a prototype working&lt;/h2&gt;
&lt;p&gt;First, let&amp;rsquo;s try out an example command.&lt;/p&gt;
&lt;p&gt;Copy-paste this into your Python 3.13 REPL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.simple_interact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Lucas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[2, 1, 3, 4, 7, 11, 18, 29]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lucas&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Lucas&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\C-n&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;lucas&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now hit &lt;code&gt;Ctrl-N&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If all worked as planned, you should see that list of numbers entered into the REPL.&lt;/p&gt;
&lt;p&gt;Cool!
Now let&amp;rsquo;s generalize this trick and make Python run our code whenever it starts.&lt;/p&gt;
&lt;p&gt;But first&amp;hellip; a disclaimer.&lt;/p&gt;
&lt;h2&gt;Here be dragons 🐉&lt;/h2&gt;
&lt;p&gt;Notice that &lt;code&gt;_&lt;/code&gt; prefix in the &lt;code&gt;_pyrepl&lt;/code&gt; module that we&amp;rsquo;re importing from?
That means this module is officially unsupported.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_pyrepl&lt;/code&gt; module is an implementation detail and its implementation may change at any time in future Python versions.&lt;/p&gt;
&lt;p&gt;In other words: &lt;code&gt;_pyrepl&lt;/code&gt; is designed to be used by &lt;em&gt;Python&amp;rsquo;s standard library modules&lt;/em&gt; and not anyone else.
That means that we should assume this code will break in a future Python version.&lt;/p&gt;
&lt;p&gt;Will that stop us from playing with this module for the fun of it?&lt;/p&gt;
&lt;p&gt;It won&amp;rsquo;t.&lt;/p&gt;
&lt;h2&gt;Creating a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file&lt;/h2&gt;
&lt;p&gt;So we&amp;rsquo;ve made &lt;em&gt;one&lt;/em&gt; custom key combination for ourselves.
How can we setup this command automatically whenever the Python REPL starts?&lt;/p&gt;
&lt;p&gt;We need a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;When Python launches, if it sees a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable it will treat that environment variable as a Python file to run on startup.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve made a &lt;code&gt;/home/trey/.python_startup.py&lt;/code&gt; file and I&amp;rsquo;ve set this environment variable in my shell&amp;rsquo;s configuration file (&lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONSTARTUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.python_startup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To start, we could put our single custom command in this file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.simple_interact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Not in the new pyrepl OR _pyrepl implementation changed&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Lucas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[2, 1, 3, 4, 7, 11, 18, 29]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lucas&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Lucas&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\C-n&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;lucas&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that I&amp;rsquo;ve stuck our code in a &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt; block.
Our code &lt;em&gt;only&lt;/em&gt; runs if those &lt;code&gt;_pyrepl&lt;/code&gt; imports succeed.&lt;/p&gt;
&lt;p&gt;Note that this &lt;em&gt;might&lt;/em&gt; still raise an exception when Python starts &lt;em&gt;if&lt;/em&gt; the reader object&amp;rsquo;s &lt;code&gt;command&lt;/code&gt; attribute or &lt;code&gt;bind&lt;/code&gt; method change in a way that breaks our code.&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;d like to see those breaking changes occur print out a traceback the next time I upgrade Python.
So I&amp;rsquo;m going to leave those last few lines &lt;em&gt;without&lt;/em&gt; their own catch-all exception handler.&lt;/p&gt;
&lt;h2&gt;Generalizing the code&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; file with a more generalized solution:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.simple_interact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Hack the new Python 3.13 REPL!&lt;/span&gt;
    &lt;span class="n"&gt;cmds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\C-n&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;[2, 1, 3, 4, 7, 11, 18, 29]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\C-f&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;[&amp;quot;apples&amp;quot;, &amp;quot;oranges&amp;quot;, &amp;quot;bananas&amp;quot;, &amp;quot;strawberries&amp;quot;, &amp;quot;pears&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;textwrap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;CustomCommand&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;            class _cmds:&lt;/span&gt;
&lt;span class="s2"&gt;                class &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(Command):&lt;/span&gt;
&lt;span class="s2"&gt;                    def do(self):&lt;/span&gt;
&lt;span class="s2"&gt;                        self.reader.insert(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;                reader.commands[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;] = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;                reader.bind(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# Clean up all the new variables&lt;/span&gt;
    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;_get_reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_cmds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This version uses a dictionary to map keyboard shortcuts to the text they should insert.&lt;/p&gt;
&lt;p&gt;Note that we&amp;rsquo;re repeatedly building up a string of &lt;code&gt;Command&lt;/code&gt; subclasses for each shortcut, using &lt;code&gt;exec&lt;/code&gt; to execute the code for that custom &lt;code&gt;Command&lt;/code&gt; subclass, and then binding the keyboard shortcut to that new command class.&lt;/p&gt;
&lt;p&gt;At the end we then delete all the variables we&amp;rsquo;ve made so our REPL will start the clean global environment we normally expect it to have:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;Python 3.13.0 (main, Oct  8 2024, 10:37:56) [GCC 11.4.0] on linux&lt;/span&gt;
&lt;span class="go"&gt;Type &amp;quot;help&amp;quot;, &amp;quot;copyright&amp;quot;, &amp;quot;credits&amp;quot; or &amp;quot;license&amp;quot; for more information.&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;__annotations__&amp;#39;, &amp;#39;__builtins__&amp;#39;, &amp;#39;__cached__&amp;#39;, &amp;#39;__doc__&amp;#39;, &amp;#39;__file__&amp;#39;, &amp;#39;__loader__&amp;#39;, &amp;#39;__name__&amp;#39;, &amp;#39;__package__&amp;#39;, &amp;#39;__spec__&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Is this messy?&lt;/p&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;Is that a needless use of a dictionary that could have been a list of 2-item tuples instead?&lt;/p&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;Does this work?&lt;/p&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;h2&gt;Doing more interesting and risky stuff&lt;/h2&gt;
&lt;p&gt;Note that there are many keyboard shortcuts that may cause weird behaviors if you bind them.&lt;/p&gt;
&lt;p&gt;For example, if you bind &lt;code&gt;Ctrl-i&lt;/code&gt;, your binding may trigger every time you try to indent.
And if you try to bind &lt;code&gt;Ctrl-m&lt;/code&gt;, your binding may be ignored because this is equivalent to hitting the &lt;code&gt;Enter&lt;/code&gt; key.&lt;/p&gt;
&lt;p&gt;So be sure to test your REPL carefully after each new binding you try to invent.&lt;/p&gt;
&lt;p&gt;If you want to do something more interesting, you could poke around in the &lt;code&gt;_pyrepl&lt;/code&gt; package to see what existing code you can use/abuse.&lt;/p&gt;
&lt;p&gt;For example, here&amp;rsquo;s a very hacky way of making a binding to &lt;code&gt;Ctrl-x&lt;/code&gt; followed by &lt;code&gt;Ctrl-r&lt;/code&gt; to make this import &lt;code&gt;subprocess&lt;/code&gt;, type in a &lt;code&gt;subprocess.run&lt;/code&gt; line, and move your cursor between the empty string within the &lt;code&gt;run&lt;/code&gt; call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;backward_kill_word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;
                &lt;span class="n"&gt;backward_kill_word&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;do&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import subprocess&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;subprocess.run(&amp;quot;&amp;quot;, shell=True)&amp;#39;&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;do&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;subprocess_run&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cmds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\C-x\C-r&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;subprocess_run&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;What keyboard shortcuts are available?&lt;/h2&gt;
&lt;p&gt;As you play with customizing keyboard shortcuts, you&amp;rsquo;ll likely notice that many key combinations result in strange and undesirable behavior when overridden.&lt;/p&gt;
&lt;p&gt;For example, overriding &lt;code&gt;Ctrl-J&lt;/code&gt; will also override the &lt;code&gt;Enter&lt;/code&gt; key&amp;hellip; at least it does in my terminal.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll list the key combinations that seem unproblematic on my setup with Gnome Terminal in Ubuntu Linux.&lt;/p&gt;
&lt;p&gt;Here are &lt;code&gt;Control&lt;/code&gt; key shortcuts that seem to be complete unused in the Python REPL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl-N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-O&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-P&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-Q&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-S&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-V&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that overriding &lt;code&gt;Ctrl-H&lt;/code&gt; is often an alternative to the backspace key&lt;/p&gt;
&lt;p&gt;Here are &lt;code&gt;Alt&lt;/code&gt;/&lt;code&gt;Meta&lt;/code&gt; key shortcuts that appear unused on my machine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Alt-A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-E&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-G&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-H&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-I&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-J&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-K&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-M&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-O&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-P&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-Q&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-S&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-V&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-W&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-X&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-Z&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can add an &lt;code&gt;Alt&lt;/code&gt; shortcut by using &lt;code&gt;\M&lt;/code&gt; (for &amp;ldquo;meta&amp;rdquo;).
So &lt;code&gt;r"\M-a"&lt;/code&gt; would capture &lt;code&gt;Alt-A&lt;/code&gt; just as &lt;code&gt;r"\C-a"&lt;/code&gt; would capture &lt;code&gt;Ctrl-A&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are keyboard shortcuts that &lt;em&gt;can&lt;/em&gt; be customized but you might want to consider whether the current default behavior is worth losing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Alt-B&lt;/code&gt;: backward word (same as &lt;code&gt;Ctrl-Left&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-C&lt;/code&gt;: capitalize word (does nothing on my machine&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-D&lt;/code&gt;: kill word (delete to end of word)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-F&lt;/code&gt;: forward word (same as &lt;code&gt;Ctrl-Right&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-L&lt;/code&gt;: downcase word (does nothing on my machine&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-U&lt;/code&gt;: upcase word (does nothing on my machine&amp;hellip;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-Y&lt;/code&gt;: yank pop&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-A&lt;/code&gt;: beginning of line (like the &lt;code&gt;Home&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-B&lt;/code&gt;: left (like the &lt;code&gt;Left&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-E&lt;/code&gt;: end of line (like the &lt;code&gt;End&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-F&lt;/code&gt;: right (like the &lt;code&gt;Right&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-G&lt;/code&gt;: cancel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-H&lt;/code&gt;: backspace (same as the &lt;code&gt;Backspace&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-K&lt;/code&gt;: kill line (delete to end of line)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-T&lt;/code&gt;: transpose characters&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-U&lt;/code&gt;: line discard (delete to beginning of line)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-W&lt;/code&gt;: word discard (delete to beginning of word)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-Y&lt;/code&gt;: yank&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt-R&lt;/code&gt;: restore history (within history mode)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What fun have you found in &lt;code&gt;_pyrepl&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;Find something fun while playing with the &lt;code&gt;_pyrepl&lt;/code&gt; package&amp;rsquo;s inner-workings?&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d love to hear about it!
Comment below to share what you found.&lt;/p&gt;</content><category term="python"/><category term="repl"/></entry><entry><title>Django and the Python 3.13 REPL</title><link href="https://treyhunner.com/2024/10/django-and-the-new-python-3-dot-13-repl/" rel="alternate"/><published>2024-10-13T21:03:32-07:00</published><updated>2024-10-13T21:03:32-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/10/django-and-the-new-python-3-dot-13-repl</id><content type="html">&lt;p&gt;Your new Django project uses Python 3.13.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re really looking forward to using the new REPL&amp;hellip; but &lt;code&gt;python manage.py shell&lt;/code&gt; just shows the same old Python REPL.
What gives?&lt;/p&gt;
&lt;p&gt;Well, Django&amp;rsquo;s management shell uses Python&amp;rsquo;s &lt;a href="https://docs.python.org/3/library/code.html"&gt;code&lt;/a&gt; module to launch a custom REPL, but the &lt;code&gt;code&lt;/code&gt; module doesn&amp;rsquo;t (&lt;a href="https://github.com/python/cpython/issues/119512"&gt;yet&lt;/a&gt;) use the new Python REPL.&lt;/p&gt;
&lt;p&gt;So you&amp;rsquo;re out of luck&amp;hellip; or are you?&lt;/p&gt;
&lt;h2&gt;How stable do you need your &lt;code&gt;shell&lt;/code&gt; command to be?&lt;/h2&gt;
&lt;p&gt;The new Python REPL&amp;rsquo;s code lives in a &lt;a href="https://github.com/python/cpython/tree/v3.13.0/Lib/_pyrepl"&gt;_pyrepl&lt;/a&gt; package.
Surely there must be some way to launch the new REPL using that &lt;code&gt;_pyrepl&lt;/code&gt; package!&lt;/p&gt;
&lt;p&gt;First, note the &lt;code&gt;_&lt;/code&gt; before that package name.
It&amp;rsquo;s &lt;code&gt;_pyrepl&lt;/code&gt;, not &lt;code&gt;pyrepl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Any solution that relies on this module may break in future Python releases.&lt;/p&gt;
&lt;p&gt;So&amp;hellip; should we give up on looking for a solution, if we can&amp;rsquo;t get a &amp;ldquo;stable&amp;rdquo; one?&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t think so.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;shell&lt;/code&gt; command doesn&amp;rsquo;t usually &lt;em&gt;need&lt;/em&gt; to be stable in more than one version of Python at a time.
So I&amp;rsquo;m fine with a solution that &lt;em&gt;attempts&lt;/em&gt; to use the new REPL and then falls back to the old REPL if it fails.&lt;/p&gt;
&lt;h2&gt;A working solution&lt;/h2&gt;
&lt;p&gt;So, let&amp;rsquo;s look at a working solution.&lt;/p&gt;
&lt;p&gt;Stick &lt;a href="https://pym.dev/p/2zqeq/"&gt;this code&lt;/a&gt; in a &lt;code&gt;management/commands/shell.py&lt;/code&gt; file within one of your Django apps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Python 3.13 REPL support using the unsupported _pyrepl module.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;django.core.management.commands.shell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;BaseShellCommand&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseShellCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;shells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ipython&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;bpython&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;pyrepl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;python&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pyrepl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;_pyrepl.main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;interactive_console&lt;/span&gt;
        &lt;span class="n"&gt;interactive_console&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;Django&amp;rsquo;s &lt;code&gt;shell&lt;/code&gt; command has made it very simple to add support for your favorite REPL of choice.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/django/django/blob/5.1.2/django/core/management/commands/shell.py"&gt;The code for the &lt;code&gt;shell&lt;/code&gt; command&lt;/a&gt; loops through the &lt;code&gt;shells&lt;/code&gt; list and attempts to run a method with that name on its own class.
If an &lt;code&gt;ImportError&lt;/code&gt; is raised then it attempts the next command, stopping once no exception occurs.&lt;/p&gt;
&lt;p&gt;Our new command will try to use IPython and bpython if they&amp;rsquo;re installed and then it will try the new Python 3.13 REPL followed by the old Python REPL.&lt;/p&gt;
&lt;p&gt;If Python 3.14 breaks our import by moving the &lt;code&gt;interactive_console&lt;/code&gt; function, then an &lt;code&gt;ImportError&lt;/code&gt; will be raised, causing us to fall back to the old REPL after we upgrade to Python 3.14 one day.
If instead, the &lt;code&gt;interactive_console&lt;/code&gt; function&amp;rsquo;s usage changes (maybe it will require arguments) then our &lt;code&gt;shell&lt;/code&gt; command will completely break and we&amp;rsquo;ll need to manually fix it when we upgrade to Python 3.14.&lt;/p&gt;
&lt;h2&gt;What&amp;rsquo;s so great about the new REPL?&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re already using IPython or BPython as your REPL and you&amp;rsquo;re enjoying them, I would stick with them.&lt;/p&gt;
&lt;p&gt;Third-party libraries move faster than Python itself and they&amp;rsquo;re often more feature-rich.
IPython has about 20 years worth of feature development and it has features that the built-in Python REPL will likely never have.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re using the default Python REPL though, this new REPL is a &lt;em&gt;huge&lt;/em&gt; upgrade.
I&amp;rsquo;ve been using it as my default REPL since May and I &lt;em&gt;love&lt;/em&gt; it.
See &lt;a href="https://pym.dev/python-313-whats-new/"&gt;my screencast on Python 3.13&lt;/a&gt; for my favorite features in the new REPL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. for Python Morsels users&lt;/strong&gt;: if you want to try using that &lt;code&gt;code&lt;/code&gt; module, check out the (fairly advanced) &lt;a href="https://www.pythonmorsels.com/exercises/3efdd9e172a346d08679ec39419ed822/?level=advanced"&gt;replr&lt;/a&gt; or (even more advanced) &lt;a href="https://www.pythonmorsels.com/exercises/5800cdcbbc5b4936b3e253dc15050480/?level=advanced"&gt;replsync&lt;/a&gt; exercises.&lt;/p&gt;</content><category term="python"/><category term="django"/><category term="repl"/></entry><entry><title>Switching from virtualenvwrapper to direnv, Starship, and uv</title><link href="https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv/" rel="alternate"/><published>2024-10-03T14:00:00-07:00</published><updated>2024-10-03T14:00:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv</id><content type="html">&lt;p&gt;Earlier this week I considered whether I should finally switch away from &lt;a href="https://virtualenvwrapper.readthedocs.io"&gt;virtualenvwrapper&lt;/a&gt; to using local &lt;code&gt;.venv&lt;/code&gt; managed by &lt;a href="https://direnv.net"&gt;direnv&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve never seriously used direnv, but I&amp;rsquo;ve been hearing &lt;a href="https://micro.webology.dev/2024/03/13/on-environment-variables.html"&gt;Jeff&lt;/a&gt; and &lt;a href="https://hynek.me/til/python-project-local-venvs/"&gt;Hynek&lt;/a&gt; talk about their use of direnv for a while.&lt;/p&gt;
&lt;p&gt;After a few days, I&amp;rsquo;ve finally stumbled into a setup that works great for me.
I&amp;rsquo;d like to note the basics of this setup as well as some fancy additions that are specific to my own use case.&lt;/p&gt;
&lt;h2&gt;My old virtualenvwrapper workflow&lt;/h2&gt;
&lt;p&gt;First, I&amp;rsquo;d like to note my &lt;em&gt;old&lt;/em&gt; workflow that I&amp;rsquo;m trying to roughly recreate:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I type &lt;code&gt;mkvenv3 &amp;lt;project_name&amp;gt;&lt;/code&gt; to create a new virtual environment for the current project directory and activate it&lt;/li&gt;
&lt;li&gt;I type &lt;code&gt;workon &amp;lt;project_name&amp;gt;&lt;/code&gt; when I want to workon that project: this activates the correct virtual environment &lt;em&gt;and&lt;/em&gt; changes to the project directory&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The initial setup I thought of allows me to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;echo layout python &amp;gt; .envrc &amp;amp;&amp;amp; direnv allow&lt;/code&gt; to create a virtual environment for the current project and activate it&lt;/li&gt;
&lt;li&gt;Change directories into the project directory to automatically activate the virtual environment&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The more complex setup I eventually settled on allows me to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;venv &amp;lt;project_name&amp;gt;&lt;/code&gt; to create a virtual environment for the current project and activate it&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;workon &amp;lt;project_name&amp;gt;&lt;/code&gt; to change directories into the project (which automatically activates the virtual environment)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The initial setup&lt;/h2&gt;
&lt;p&gt;First, I &lt;a href="https://direnv.net"&gt;installed direnv&lt;/a&gt; and added this to my &lt;code&gt;~/.zshrc&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;direnv&lt;span class="w"&gt; &lt;/span&gt;hook&lt;span class="w"&gt; &lt;/span&gt;zsh&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then whenever I wanted to create a virtual environment for a new project I created a &lt;code&gt;.envrc&lt;/code&gt; file in that directory, which looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;layout&lt;span class="w"&gt; &lt;/span&gt;python
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then I ran &lt;code&gt;direnv allow&lt;/code&gt; to allow, as &lt;code&gt;direnv&lt;/code&gt; instructed me to, to allow the new virtual environment to be automatically created and activated.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s pretty much it.&lt;/p&gt;
&lt;p&gt;Unfortunately, I did not like this initial setup.&lt;/p&gt;
&lt;h2&gt;No shell prompt?&lt;/h2&gt;
&lt;p&gt;The first problem was that the virtual environment&amp;rsquo;s prompt didn&amp;rsquo;t show up in my shell prompt.
This is due to a &lt;a href="https://github.com/direnv/direnv/issues/529"&gt;direnv not allowing modification of the &lt;code&gt;PS1&lt;/code&gt; shell prompt&lt;/a&gt;.
That means I&amp;rsquo;d need to modify my shell configuration to show the correct virtual environment name myself.&lt;/p&gt;
&lt;p&gt;So I added this to my &lt;code&gt;~/.zshrc&lt;/code&gt; file to show the virtual environment name at the beginning of my prompt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add direnv-activated venv to prompt&lt;/span&gt;
show_virtual_env&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV_PROMPT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DIRENV_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV_PROMPT&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;) &amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;PS1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$(show_virtual_env)&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$PS1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Wrong virtual environment directory&lt;/h2&gt;
&lt;p&gt;The next problem was that the virtual environment was placed in &lt;code&gt;.direnv/python3.12&lt;/code&gt;.
I wanted each virtual environment to be in a &lt;code&gt;.venv&lt;/code&gt; directory instead.&lt;/p&gt;
&lt;p&gt;To do that, I made a &lt;code&gt;.config/direnv/direnvrc&lt;/code&gt; file that customized the python layout:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;layout_python&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.venv&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.venv&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;log_status&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;No virtual environment exists. Executing \`python -m venv .venv\`.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;.venv
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.venv&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Activate the virtual environment&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;/bin/activate
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Loading, unloading, loading, unloading&amp;hellip;&lt;/h2&gt;
&lt;p&gt;I also didn&amp;rsquo;t like the loading and unloading messages that showed up each time I changed directories.
I removed those by clearing the &lt;code&gt;DIRENV_LOG_FORMAT&lt;/code&gt; variable in my &lt;code&gt;~/.zshrc&lt;/code&gt; configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DIRENV_LOG_FORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;The more advanced setup&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t like it when all my virtual environment prompts show up as &lt;code&gt;.venv&lt;/code&gt;.
I want ever prompt to be the name of the actual project&amp;hellip; which is usually the directory name.&lt;/p&gt;
&lt;p&gt;I also &lt;em&gt;really&lt;/em&gt; wanted to be able to type &lt;code&gt;venv&lt;/code&gt; to create a new virtual environment, activate it, and create the &lt;code&gt;.envrc&lt;/code&gt; file for my &lt;em&gt;automatically&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Additionally, I thought it would be really handy if I could type &lt;code&gt;workon &amp;lt;project_name&amp;gt;&lt;/code&gt; to change directories to a specific project.&lt;/p&gt;
&lt;p&gt;I made two aliases in my &lt;code&gt;~/.zshrc&lt;/code&gt; configuration for all of this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;venv&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;venv_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;projects_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.projects&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Check if .envrc already exists&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;.envrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: .envrc already exists&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Create venv&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;--prompt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$venv_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: Failed to create venv&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Create .envrc&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;layout python&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;.envrc

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Append project name and directory to projects file&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;venv_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PWD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$projects_file&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Allow direnv to immediately activate the virtual environment&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;direnv&lt;span class="w"&gt; &lt;/span&gt;allow
&lt;span class="o"&gt;}&lt;/span&gt;

workon&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;projects_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.projects&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;project_dir

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Check for projects config file&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$projects_file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: &lt;/span&gt;&lt;span class="nv"&gt;$projects_file&lt;/span&gt;&lt;span class="s2"&gt; not found&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Get the project directory for the given project name&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;project_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;^&lt;/span&gt;&lt;span class="nv"&gt;$project_name&lt;/span&gt;&lt;span class="s2"&gt;\s*=&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$projects_file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/^[^=]*=\s*//&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Ensure a project directory was found&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$project_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: Project &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$project_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; not found in &lt;/span&gt;&lt;span class="nv"&gt;$projects_file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Ensure the project directory exists&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$project_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: Directory &lt;/span&gt;&lt;span class="nv"&gt;$project_dir&lt;/span&gt;&lt;span class="s2"&gt; does not exist&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Change directories&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$project_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now I can type this to create a &lt;code&gt;.venv&lt;/code&gt; virtual environment in my current directory, which has a prompt named after the current directory, activate it, and create a &lt;code&gt;.envrc&lt;/code&gt; file which will automatically activate that virtual environment (thanks to that &lt;code&gt;~/.config/direnv/direnvrc&lt;/code&gt; file) whenever I change into that directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If I wanted to customized the prompt name for the virtual environment, I could do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;my_project
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When I wanted to start working on that project later, I can either change into that directory &lt;em&gt;or&lt;/em&gt; if I&amp;rsquo;m feeling lazy I can simply type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;workon&lt;span class="w"&gt; &lt;/span&gt;my_project
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That reads from my &lt;code&gt;~/.projects&lt;/code&gt; file to look up the project directory to switch to.&lt;/p&gt;
&lt;h2&gt;Switching to uv&lt;/h2&gt;
&lt;p&gt;I also decided to try using &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; for all of this, since it&amp;rsquo;s faster at creating virtual environments.
One benefit of &lt;code&gt;uv&lt;/code&gt; is that it tries to select the correct Python version for the project, if it sees a version noted in a &lt;code&gt;pyproject.toml&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Another benefit of using &lt;code&gt;uv&lt;/code&gt;, is that I should also be able to update the &lt;code&gt;venv&lt;/code&gt; to use a specific version of Python with something like &lt;code&gt;--python 3.12&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are the updated shell aliases for the &lt;code&gt;~/.zshrc&lt;/code&gt; for &lt;code&gt;uv&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;venv&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;venv_name
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;dir_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# If there are no arguments or the last argument starts with a dash, use dir_name&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!#&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;venv_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$dir_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;venv_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!#&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@:&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="p"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Check if .envrc already exists&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;.envrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: .envrc already exists&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Create venv using uv with all passed arguments&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;--seed&lt;span class="w"&gt; &lt;/span&gt;--prompt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$venv_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: Failed to create venv&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Create .envrc&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;layout python&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;.envrc

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Append to ~/.projects&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;venv_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PWD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;~/.projects

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Allow direnv to immediately activate the virtual environment&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;direnv&lt;span class="w"&gt; &lt;/span&gt;allow
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Switching to starship&lt;/h2&gt;
&lt;p&gt;I also decided to try out using &lt;a href="https://starship.rs"&gt;Starship&lt;/a&gt; to customize my shell this week.&lt;/p&gt;
&lt;p&gt;I added this to my &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;starship&lt;span class="w"&gt; &lt;/span&gt;init&lt;span class="w"&gt; &lt;/span&gt;zsh&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And removed this, which is no longer needed since Starship will be managing the shell for me:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add direnv-activated venv to prompt&lt;/span&gt;
show_virtual_env&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV_PROMPT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DIRENV_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV_PROMPT&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;) &amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;PS1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$(show_virtual_env)&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$PS1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also switched my &lt;code&gt;python&lt;/code&gt; layout for direnv to just set the &lt;code&gt;$VIRTUAL_ENV&lt;/code&gt; variable and add the &lt;code&gt;$VIRTUAL_ENV/bin&lt;/code&gt; directory to my &lt;code&gt;PATH&lt;/code&gt;, since the &lt;code&gt;$VIRTUAL_ENV_PROMPT&lt;/code&gt; variable isn&amp;rsquo;t needed for Starship to pick up the prompt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;layout_python&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.venv&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;PATH_add&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;&lt;span class="s2"&gt;/bin&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;VIRTUAL_ENV
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also made a &lt;em&gt;very&lt;/em&gt; boring Starship configuration in &lt;code&gt;~/.config/starship.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;$python&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;$directory&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;$git_branch&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;$git_state&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;$character&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;add_newline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;[python]&lt;/span&gt;
&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;([(\($virtualenv\) )]($style))&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bright-black&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[directory]&lt;/span&gt;
&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bright-blue&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[character]&lt;/span&gt;
&lt;span class="n"&gt;success_symbol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;$](black)&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;error_symbol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;$](bright-red)&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;vimcmd_symbol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[❮](green)&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[git_branch]&lt;/span&gt;
&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[$symbol$branch]($style) &amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bright-purple&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[git_state]&lt;/span&gt;
&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\([$state( $progress_current/$progress_total)]($style)\) &amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;purple&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[cmd_duration.disabled]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I setup such a boring configuration because when I&amp;rsquo;m teaching, I don&amp;rsquo;t want my students to be confused or distracted by a prompt that has considerably more information in it than &lt;em&gt;their&lt;/em&gt; default prompt may have.&lt;/p&gt;
&lt;p&gt;The biggest downside of switching to Starship has been my own earworm-oriented brain.
As I update my Starship configuration files, I&amp;rsquo;ve repeatedly heard David Bowie singing &amp;ldquo;I&amp;rsquo;m a Starmaaan&amp;rdquo;. 🎶&lt;/p&gt;
&lt;h2&gt;Ground control to major TOML&lt;/h2&gt;
&lt;p&gt;After all of that, I realized that I could additionally use different Starship configurations for different directories by putting a &lt;code&gt;STARSHIP_CONFIG&lt;/code&gt; variable in specific layouts.
After that realization, I made my configuration even &lt;em&gt;more&lt;/em&gt; vanilla and made some alternative configurations in my &lt;code&gt;~/.config/direnv/direnvrc&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;layout_python&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;VIRTUAL_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.venv&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;PATH_add&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;&lt;span class="s2"&gt;/bin&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;VIRTUAL_ENV

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;STARSHIP_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/trey/.config/starship/python.toml
&lt;span class="o"&gt;}&lt;/span&gt;

layout_git&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;STARSHIP_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/trey/.config/starship/git.toml
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Those other two configuration files are fancier, as I have no concern about them distracting my students since I&amp;rsquo;ll never be within those directories while teaching.&lt;/p&gt;
&lt;p&gt;You can find those files in &lt;a href="https://github.com/treyhunner/dotfiles"&gt;my dotfiles repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The necessary tools&lt;/h2&gt;
&lt;p&gt;So I replaced virtualenvwrapper with direnv, uv, and Starship.
Though direnv is doing most of the important work here.
The use of uv and Starship were just bonuses.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;am&lt;/em&gt; also hoping to eventually replace my pipx use with uv and once uv supports &lt;a href="https://github.com/astral-sh/uv/issues/6265"&gt;adding python3.x commands&lt;/a&gt; to my &lt;code&gt;PATH&lt;/code&gt;, I may replace my use of pyenv with uv as well.&lt;/p&gt;
&lt;p&gt;Thanks to all who &lt;a href="https://mastodon.social/@treyhunner/113232640710715449"&gt;participated in my Mastodon thread&lt;/a&gt; as I fumbled through discovering this setup.&lt;/p&gt;</content><category term="python"/><category term="direnv"/><category term="uv"/><category term="starship"/><category term="dotfiles"/></entry><entry><title>10-Week Hands-On Python Course</title><link href="https://treyhunner.com/2024/08/python-high-five/" rel="alternate"/><published>2024-08-20T14:20:00-07:00</published><updated>2024-08-20T14:20:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/08/python-high-five</id><content type="html">&lt;p&gt;Ever wished you could take an &lt;strong&gt;Intro to Python training&lt;/strong&gt; with me, but you don&amp;rsquo;t work for a company with a generous training budget?
I&amp;rsquo;m running a Python-learning program just for this situation.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.pythonmorsels.com/high-five/"&gt;Python High Five&lt;/a&gt; is a 10-week Python jumpstart program that starts &lt;strong&gt;this September&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Set aside the time to learn ⌚&lt;/h2&gt;
&lt;p&gt;One of the biggest problems for folks starting to learn Python is &lt;strong&gt;setting aside the time&lt;/strong&gt;.
And even if you &lt;em&gt;do&lt;/em&gt; manage to set aside the time, you&amp;rsquo;ll often hit a roadblock where you feel &lt;strong&gt;confused&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Python High Five is a way to keep a &lt;strong&gt;daily learning habit&lt;/strong&gt; &lt;em&gt;and&lt;/em&gt; to &lt;strong&gt;get help&lt;/strong&gt; when find yourself stuck.&lt;/p&gt;
&lt;p&gt;This program is based around &lt;strong&gt;daily practice&lt;/strong&gt;.
Monday through Friday you&amp;rsquo;ll pick &lt;strong&gt;30 minutes&lt;/strong&gt; from your schedule, at any time that works you.
During those 30 minutes, you&amp;rsquo;ll watch a 5 minute video, work on the day&amp;rsquo;s exercise, and reflect on your progress.&lt;/p&gt;
&lt;h2&gt;The most effective learning is hands-on 🖐️&lt;/h2&gt;
&lt;p&gt;Python High Five is all about learning through &lt;strong&gt;writing Python code&lt;/strong&gt;.
Each week we&amp;rsquo;ll dive deeper into Python, building upon what we&amp;rsquo;ve learned so far.&lt;/p&gt;
&lt;p&gt;When you find yourself stuck you can get help through an asynchronous &lt;strong&gt;group chat&lt;/strong&gt; and weekly &lt;strong&gt;office hour&lt;/strong&gt; sessions.
In addition to our weekly office hours together, I&amp;rsquo;ll check the chat each day, respond to questions, and provide guidance.&lt;/p&gt;
&lt;h2&gt;Proven learning techniques behind the scenes 📝&lt;/h2&gt;
&lt;p&gt;The daily check-ins allow for daily &lt;strong&gt;accountability&lt;/strong&gt;.
The group chat also provides both a community of peers to rely on, and &lt;strong&gt;guidance from an experienced Python trainer&lt;/strong&gt; (me).&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll also be using proven learning techniques behind the scenes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Retrieval practice&lt;/strong&gt;: you don&amp;rsquo;t learn by putting information into your head, but by trying to take it out; for Python learning, that means writing code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spaced repetition&lt;/strong&gt;: cramming is less effective than learning spaced out over time, which is why we&amp;rsquo;ll spend 30 minutes each weekday instead of spending a few hours every week.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interleaving&lt;/strong&gt;: each day&amp;rsquo;s exercise isn&amp;rsquo;t predictably themed because a bit of unpredictability can be &lt;em&gt;really&lt;/em&gt; improve learning outcomes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Elaboration&lt;/strong&gt;: your daily check-in isn&amp;rsquo;t &lt;em&gt;just&lt;/em&gt; about reflection: it&amp;rsquo;s also a helpful learning tool!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus, we&amp;rsquo;ll be working through curriculum I&amp;rsquo;ve been developing and iterating on for many years.
I have taught these topics in many different settings to folks from &lt;em&gt;many&lt;/em&gt; different backgrounds.&lt;/p&gt;
&lt;h2&gt;Form a daily learning habit 🔁&lt;/h2&gt;
&lt;p&gt;Any 10-week program will be &lt;em&gt;just the start&lt;/em&gt; of a Python learning habit.
You&amp;rsquo;ll need to keep up your Python after Python High Five ends, either by promptly applying your skills to a new project or diving deeper into Python with continued daily practice.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why I&amp;rsquo;m offering an &lt;strong&gt;80% discount&lt;/strong&gt; for High Five attendees on one year of &lt;a href="https://www.pythonmorsels.com"&gt;Python Morsels&lt;/a&gt;, which is my skill-building service designed to help &lt;strong&gt;deepen your Python skills every week&lt;/strong&gt;.
You can &lt;a href="https://www.pythonmorsels.com/high-five/#morsels"&gt;see more details on that here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Ready to start your Python journey? ⛰️&lt;/h2&gt;
&lt;p&gt;Are you ready to start your Python journey with a solid foundation?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.pythonmorsels.com/high-five/"&gt;Read more about Python High Five&lt;/a&gt; and decide whether this is for you.&lt;/p&gt;
&lt;p&gt;Keep in mind that while the program begins on September 9, &lt;strong&gt;enrollment closes on August 31&lt;/strong&gt;.
So &lt;a href="https://www.pythonmorsels.com/high-five/#faq"&gt;check the FAQs&lt;/a&gt; and if you have additional questions, be sure to email me soon!&lt;/p&gt;</content><category term="python"/></entry><entry><title>Quickly find the right datetime format code for your date</title><link href="https://treyhunner.com/2024/08/find-the-datetime-format-code-for-your-date/" rel="alternate"/><published>2024-08-05T11:30:00-07:00</published><updated>2024-08-05T11:30:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/08/find-the-datetime-format-code-for-your-date</id><content type="html">&lt;p&gt;I often find myself with a string representing a date and time and the need to create a format string that will parse this string into a &lt;code&gt;datetime&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;I decided to make a tool that solves this problem for me: https://pym.dev/strptime&lt;/p&gt;
&lt;h2&gt;Finding the code to parse a date format with &lt;code&gt;strptime&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s how I&amp;rsquo;m now using this new tool.&lt;/p&gt;
&lt;p&gt;I find a date string in a random spreadsheet or log file that I need to parse.
For example, the string &lt;code&gt;30-Jun-2024 20:09&lt;/code&gt;, which I recently found in a spreadsheet.&lt;/p&gt;
&lt;p&gt;I then paste the string into the tool and watch the format appear:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pym.dev/strptime"&gt;&lt;img alt="" src="/images/strptime-1.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then I click on the date format to copy-paste it.
That&amp;rsquo;s it!&lt;/p&gt;
&lt;p&gt;This tool works by cycling through a number of common formats.
It also works for dates without a time, like &lt;code&gt;Jul 1, 2024&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pym.dev/strptime"&gt;&lt;img alt="" src="/images/strptime-2.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This input field works great when you&amp;rsquo;re in need of a code for the &lt;code&gt;datetime&lt;/code&gt; class&amp;rsquo;s &lt;code&gt;strptime&lt;/code&gt; method (which &lt;em&gt;parses&lt;/em&gt; dates).
But what if you need a code for &lt;code&gt;strftime&lt;/code&gt; (for &lt;em&gt;formatting&lt;/em&gt; dates)?&lt;/p&gt;
&lt;h2&gt;Finding the code to format a date with &lt;code&gt;strftime&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;If you don&amp;rsquo;t have a date but instead want to &lt;em&gt;construct&lt;/em&gt; a date in a specific common format, scroll down the page a bit.&lt;/p&gt;
&lt;p&gt;This page includes a table of common formats.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pym.dev/strptime#formats"&gt;&lt;img alt="" src="/images/strptime-table.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Click on the format to copy it.
That&amp;rsquo;s it.&lt;/p&gt;
&lt;h2&gt;Playing with format codes&lt;/h2&gt;
&lt;p&gt;What if you have a date format already but you&amp;rsquo;re not sure what it represents?&lt;/p&gt;
&lt;p&gt;Paste it in the box!&lt;/p&gt;
&lt;p&gt;For example if you&amp;rsquo;re wondering what the &lt;code&gt;%B&lt;/code&gt; in &lt;code&gt;%B %d, %Y&lt;/code&gt; means, paste it in to see what that represent with the current date and time:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pym.dev/strptime"&gt;&lt;img alt="" src="/images/strptime-reverse.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Other features&lt;/h2&gt;
&lt;p&gt;There are a few other hidden features in this tool:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After a date or date format is pasted, if it corresponds to one of the formats listed in the table of common formats, that row will be highlighted&lt;/li&gt;
&lt;li&gt;Hitting the &lt;code&gt;Enter&lt;/code&gt; key anywhere on the page will select the input field&lt;/li&gt;
&lt;li&gt;Clicking on a date within the format table will fill that date into the input box&lt;/li&gt;
&lt;li&gt;The bottom of the page includes links to other useful datetime formatting/parsing tools as well as a link to the relevant Python documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Thoughts? Feature requests?&lt;/h2&gt;
&lt;p&gt;What do you think of this tool?&lt;/p&gt;
&lt;p&gt;Is this something you&amp;rsquo;d bookmark and use often?
Is this missing a key feature that you would need for it to be valuable for your use?&lt;/p&gt;
&lt;p&gt;Are there date and time formats you&amp;rsquo;d like to see that don&amp;rsquo;t seem to be supported yet?&lt;/p&gt;
&lt;p&gt;Comment or email me to let me know!&lt;/p&gt;</content><category term="python"/></entry><entry><title>Why does "python -m json" not work? Why is it "json.tool"?</title><link href="https://treyhunner.com/2024/08/why-does-python-m-json-not-work/" rel="alternate"/><published>2024-08-01T14:00:00-07:00</published><updated>2024-08-01T14:00:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/08/why-does-python-m-json-not-work</id><content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: upon the encouragement of a few CPython core team members, I &lt;a href="https://github.com/python/cpython/pull/122884"&gt;opened a pull request&lt;/a&gt; to add this to Python 3.14.&lt;/p&gt;
&lt;p&gt;Have you ever used Python&amp;rsquo;s &lt;code&gt;json.tool&lt;/code&gt; command-line interface?&lt;/p&gt;
&lt;p&gt;You can run &lt;code&gt;python -m json.tool&lt;/code&gt; against a JSON file and Python will print a nicely formatted version of the file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;json.tool&lt;span class="w"&gt; &lt;/span&gt;example.json
&lt;span class="o"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;,
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;,
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;,
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Why do we need to run &lt;code&gt;json.tool&lt;/code&gt; instead of &lt;code&gt;json&lt;/code&gt;?&lt;/p&gt;
&lt;h2&gt;The history of &lt;code&gt;python -m&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Python 3.5 added the ability to run a module as a command-line script using the &lt;code&gt;-m&lt;/code&gt; argument (see &lt;a href="https://peps.python.org/pep-0338/"&gt;PEP 338&lt;/a&gt;) which was implemented in Python 2.5.
While that feature was being an additional feature/bug was accidentally added, alowing packages to be run with the &lt;code&gt;-m&lt;/code&gt; flag as well.
When a package was run with the &lt;code&gt;-m&lt;/code&gt; flag, the package&amp;rsquo;s &lt;code&gt;__init__.py&lt;/code&gt; file would be run, with the &lt;code&gt;__name__&lt;/code&gt; variable set to &lt;code&gt;__main__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This accidental feature/bug was &lt;a href="https://github.com/python/cpython/issues/47000"&gt;removed in Python 2.6&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It wasn&amp;rsquo;t until Python 2.7 that the ability to run &lt;code&gt;python -m package&lt;/code&gt; was re-added (see below).&lt;/p&gt;
&lt;h2&gt;The history of the &lt;code&gt;json&lt;/code&gt; module&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;json&lt;/code&gt; module was &lt;a href="https://docs.python.org/3/whatsnew/2.6.html#the-json-module-javascript-object-notation"&gt;added in Python 2.6&lt;/a&gt;.
It was based on the third-party &lt;code&gt;simplejson&lt;/code&gt; library.&lt;/p&gt;
&lt;p&gt;That library originally relied on the fact that Python packages could be run with the &lt;code&gt;-m&lt;/code&gt; flag to run the package&amp;rsquo;s &lt;code&gt;__init__.py&lt;/code&gt; file with &lt;code&gt;__name__&lt;/code&gt; set to &lt;code&gt;__main__&lt;/code&gt; (see &lt;a href="https://github.com/simplejson/simplejson/blob/v1.8.1/simplejson/__init__.py#L368"&gt;this code from version 1.8.1&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;simplejson&lt;/code&gt; was added to Python as the &lt;code&gt;json&lt;/code&gt; module in Python 2.6, this bug/feature could no longer be relied upon as it was fixed in Python 2.6.
To continue allowing for a nice command-line interface, it was decided that running a &lt;code&gt;tool&lt;/code&gt; submodule would be the way to add a command-line interface to the &lt;code&gt;json&lt;/code&gt; package (&lt;a href="https://github.com/simplejson/simplejson/commit/74d9c5c4c4339db47dfa86bf37858cae80ed3776"&gt;discussion here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Python 2.7 &lt;a href="https://docs.python.org/2.7/using/cmdline.html?highlight=__main__#cmdoption-m"&gt;added the ability to run any package as a command-line tool&lt;/a&gt;.
The package would simply need a &lt;code&gt;__main__.py&lt;/code&gt; file within it, which Python would run as the entry point to the package.&lt;/p&gt;
&lt;p&gt;At this point, &lt;code&gt;json.tool&lt;/code&gt; had already been added in Python 2.6 and no attempt was made (as far as I can tell) to allow &lt;code&gt;python -m json&lt;/code&gt; to work instead.
Once you&amp;rsquo;ve added a feature, removing or changing it can be painful.&lt;/p&gt;
&lt;h2&gt;Could we make &lt;code&gt;python -m json&lt;/code&gt; work today?&lt;/h2&gt;
&lt;p&gt;We could.
We would just need to &lt;a href="https://github.com/treyhunner/cpython/commit/1226315e2df0d4229558734d5f0d50f1386a025e"&gt;rename &lt;code&gt;tool.py&lt;/code&gt; to &lt;code&gt;__main__.py&lt;/code&gt;&lt;/a&gt;.
To allow &lt;code&gt;json.tool&lt;/code&gt; to still work &lt;em&gt;also&lt;/em&gt;, would could &lt;a href="https://github.com/python/cpython/commit/7ce95d21886c7ad5278c07c1a20cda5bebab4731"&gt;make a new &lt;code&gt;tool.py&lt;/code&gt; module&lt;/a&gt; that simply imports &lt;code&gt;json.__main__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We could even go so far as to &lt;a href="https://github.com/treyhunner/cpython/commit/ae4ca62346c690e1c6aaf1ccfed37069984b5d67"&gt;deprecate &lt;code&gt;json.tool&lt;/code&gt;&lt;/a&gt; if we wanted to.&lt;/p&gt;
&lt;p&gt;Should we do this though? 🤔&lt;/p&gt;
&lt;h2&gt;Should we make &lt;code&gt;python -m json&lt;/code&gt; work?&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t know about you, but I would rather type &lt;code&gt;python -m json&lt;/code&gt; than &lt;code&gt;python -m json.tool&lt;/code&gt;.
It&amp;rsquo;s more memorable &lt;em&gt;and&lt;/em&gt; easier to guess, if you&amp;rsquo;re not someone who has memorized &lt;a href="https://www.pythonmorsels.com/cli-tools/"&gt;all the command-line tools hiding in Python&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But would this actually be used?
I mean, don&amp;rsquo;t people just use the &lt;a href="https://jqlang.github.io/jq/"&gt;jq&lt;/a&gt; tool instead?&lt;/p&gt;
&lt;p&gt;Well, a &lt;code&gt;sqlite3&lt;/code&gt; shell was &lt;a href="https://docs.python.org/3/library/sqlite3.html#command-line-interface"&gt;added to Python 3.12&lt;/a&gt; despite the fact that third-party interactive sqlite prompts are fairly common.&lt;/p&gt;
&lt;p&gt;It is pretty handy to have a access to a tool within an unfamiliar environment where installing yet-another-tool might pose a problem.
Think Docker, a locked-down Windows machine, or any computer without network or with network restrictions.&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;d like to see &lt;code&gt;python3 -m json&lt;/code&gt; work.
I can&amp;rsquo;t think of any big downsides.
Can you?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: &lt;a href="https://github.com/python/cpython/pull/122884"&gt;pull request opened&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Too long; didn&amp;rsquo;t read&lt;/h2&gt;
&lt;p&gt;The &amp;ldquo;too long didn&amp;rsquo;t read version&amp;rdquo;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 2.5 added support for the &lt;code&gt;-m&lt;/code&gt; argument for &lt;em&gt;modules&lt;/em&gt;, but not &lt;em&gt;packages&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;A third-party &lt;code&gt;simplejson&lt;/code&gt; app existed with a nice CLI that relied on a &lt;code&gt;-m&lt;/code&gt; implementation bug allowing packages to be run using &lt;code&gt;-m&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Python 2.6 fixed that implementation quirk and broke the previous ability to run &lt;code&gt;python -m simplejson&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Python 2.6 also added the &lt;code&gt;json&lt;/code&gt; module, which was based on this third-party &lt;code&gt;simplejson&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;python -m json&lt;/code&gt; couldn&amp;rsquo;t work anymore, the ability to run &lt;code&gt;python -m json.tool&lt;/code&gt; was added&lt;/li&gt;
&lt;li&gt;Python 2.7 added official support for &lt;code&gt;python -m some_package&lt;/code&gt; by running a &lt;code&gt;__main__&lt;/code&gt; submodule&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;json.tool&lt;/code&gt; module already existed in Python 2.6 and the ability to run &lt;code&gt;python -m json&lt;/code&gt; was (as far as I can tell) never seriously considered&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;All thanks to git history and issue trackers&lt;/h2&gt;
&lt;p&gt;I discovered this by noting &lt;a href="https://github.com/simplejson/simplejson/commit/74d9c5c4c4339db47dfa86bf37858cae80ed3776"&gt;the first commit&lt;/a&gt; that added the &lt;code&gt;tool&lt;/code&gt; submodule to &lt;code&gt;simplejson&lt;/code&gt;, which notes the fact that this was for consistency with the new &lt;code&gt;json&lt;/code&gt; standard library module.&lt;/p&gt;
&lt;p&gt;Thank you git history.
And thank you to the folks who brought us the &lt;code&gt;simplejson&lt;/code&gt; library, the &lt;code&gt;json&lt;/code&gt; module, and the ability to use &lt;code&gt;-m&lt;/code&gt; on both a module and a package!&lt;/p&gt;
&lt;p&gt;Also, thank you to Alyssa Coghlan, Hugo van Kemenade, and Adam Turner for reviewing my pull request to add this feature to Python 3.14. 💖&lt;/p&gt;</content><category term="python"/></entry><entry><title>A beautiful Python monstrosity</title><link href="https://treyhunner.com/2024/06/a-beautiful-python-monstrosity/" rel="alternate"/><published>2024-06-08T14:30:00-07:00</published><updated>2024-06-08T14:30:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/06/a-beautiful-python-monstrosity</id><content type="html">&lt;p&gt;Creating performance tests for &lt;a href="https://www.pythonmorsels.com"&gt;Python Morsels&lt;/a&gt; exercises is a frequent annoyance&lt;/p&gt;
&lt;p&gt;I loathe writing automated tests for performance-related exercises because they&amp;rsquo;re &lt;em&gt;always&lt;/em&gt; flaky.
How flaky depends on the exercise, what I&amp;rsquo;m testing, and the time variability inherent in the particular Python features that a learner might use.&lt;/p&gt;
&lt;p&gt;I came up with a solution for flaky tests recently, but it also makes my tests less readable.
I then came up with a tool to improve the readability, but that has its own trade-offs.&lt;/p&gt;
&lt;p&gt;The code I eventually came up with is a &lt;strong&gt;beautiful Python monstrosity&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="nd"&gt;@attempt_n_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;
        &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I&amp;rsquo;ll explain what that code does, but first let&amp;rsquo;s talk about why it&amp;rsquo;s needed.&lt;/p&gt;
&lt;h2&gt;The flaky performance tests&lt;/h2&gt;
&lt;p&gt;My flaky performance tests initially looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_some_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.04&lt;/span&gt;

    &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;small_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;medium_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first block runs a performance test for the user&amp;rsquo;s function on a very small list and on a slightly larger list and then asserting that the slightly larger list didn&amp;rsquo;t take &lt;em&gt;too&lt;/em&gt; much longer to run.
The next two blocks run the same code on even larger lists and make further assertions about the relative times that the code took to run.&lt;/p&gt;
&lt;p&gt;This roughly approximates the &lt;a href="https://www.pythonmorsels.com/time-complexities/"&gt;time complexity&lt;/a&gt; of this code.&lt;/p&gt;
&lt;h2&gt;Running performance checks in a loop&lt;/h2&gt;
&lt;p&gt;These performance checks need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Predictably fail for inefficient solutions&lt;/li&gt;
&lt;li&gt;Predictably pass for efficient solutions&lt;/li&gt;
&lt;li&gt;Run &lt;em&gt;fast&lt;/em&gt; (within just a few seconds) even when the code is inefficient&lt;/li&gt;
&lt;li&gt;Avoid the use of &lt;code&gt;threading&lt;/code&gt; because they&amp;rsquo;ll be running on WebAssembly in the browser&lt;/li&gt;
&lt;li&gt;Run consistently on pretty much any computer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These 5 requirements together have caused me countless headaches.
I get the tests passing well, but they don&amp;rsquo;t always fail when they should.
I get the tests failing and passing when they should, but then they&amp;rsquo;re too slow.
And so on&amp;hellip;&lt;/p&gt;
&lt;p&gt;Notice the &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;m&lt;/code&gt; factors in the above assertions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;m&lt;/code&gt; are too big, we&amp;rsquo;ll get false positives (tests passing when they should fail).
If &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;m&lt;/code&gt; are too small, we&amp;rsquo;ll get false negatives (tests failing when they should pass).&lt;/p&gt;
&lt;p&gt;To avoid both &lt;a href="https://en.wikipedia.org/wiki/Type_I_and_type_II_errors"&gt;Type I and Type II errors&lt;/a&gt;, I decided to keep &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;m&lt;/code&gt; small but attempt the assertion block multiple times.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the (far less flaky) revised code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_some_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.04&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;small_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;medium_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;for&lt;/code&gt; loop runs the code multiple times, the &lt;code&gt;break&lt;/code&gt; statement stops the code as soon as the assertions all pass, and the &lt;code&gt;except&lt;/code&gt; and &lt;code&gt;if&lt;/code&gt; ensure that any assertion errors are suppressed until/unless we&amp;rsquo;re on the final iteration of the loop.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s call this a &lt;code&gt;for&lt;/code&gt;-&lt;code&gt;try&lt;/code&gt;-&lt;code&gt;break&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;if&lt;/code&gt;-&lt;code&gt;raise&lt;/code&gt; pattern.
It&amp;rsquo;s an absurdly verbose name fitting of absurdly verbose code.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;for&lt;/code&gt;-&lt;code&gt;try&lt;/code&gt;-&lt;code&gt;break&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;if&lt;/code&gt;-&lt;code&gt;raise&lt;/code&gt; pattern works pretty well!
But it&amp;rsquo;s not pretty.&lt;/p&gt;
&lt;p&gt;Like many programmers, I believe that &lt;strong&gt;Don&amp;rsquo;t Repeat Yourself&lt;/strong&gt; (&lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY&lt;/a&gt;) need not apply to tests.
Tests are &lt;em&gt;allowed&lt;/em&gt; to be repetitive if &lt;a href="https://stackoverflow.com/questions/6453235/what-does-damp-not-dry-mean-when-talking-about-unit-tests"&gt;the verbosity improves readability&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But there is &lt;em&gt;so much noise&lt;/em&gt; in that code!
I decided that removing some noise might improve readability.
So I devised a helper utility to reduce the repetition.&lt;/p&gt;
&lt;h2&gt;In search of a solution&lt;/h2&gt;
&lt;p&gt;While pondering the repetitive noise in this code, I wondered what Python features I could use to abstract away this &lt;code&gt;for&lt;/code&gt;-&lt;code&gt;try&lt;/code&gt;-&lt;code&gt;break&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;if&lt;/code&gt;-&lt;code&gt;raise&lt;/code&gt; pattern.&lt;/p&gt;
&lt;p&gt;Could I make a context manager and use a &lt;code&gt;with&lt;/code&gt; block?
That might help with the &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;, but context managers can&amp;rsquo;t run their code block multiple times, so that wouldn&amp;rsquo;t help with the &lt;code&gt;for&lt;/code&gt; and the &lt;code&gt;break&lt;/code&gt;.
So a context manager is out.&lt;/p&gt;
&lt;p&gt;Could I abstract this away into a looping helper by implementing a generator function?
We &lt;em&gt;are&lt;/em&gt; looping and generator functions &lt;em&gt;can&lt;/em&gt; &lt;code&gt;break&lt;/code&gt; early.
But, a generator function can&amp;rsquo;t catch an exception that&amp;rsquo;s raised within the &lt;em&gt;body&lt;/em&gt; of a loop.
So a generator function wouldn&amp;rsquo;t work either.&lt;/p&gt;
&lt;p&gt;What about a decorator? 🤔&lt;/p&gt;
&lt;p&gt;Context managers and decorators both sandwich a block of code.
But decorators sandwich functions and they have the power to run the same function &lt;em&gt;repeatedly&lt;/em&gt;.
A decorator might work!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a decorator that will run a given function up to 10 times (until no &lt;code&gt;AssertionError&lt;/code&gt; is raised):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;try_10_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To use this decorator, we would need to define a function and then call that function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@try_10_times&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This isn&amp;rsquo;t &lt;em&gt;quite&lt;/em&gt; good enough though&amp;hellip;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We need a pattern to run code N times (not necessarily exactly 10)&lt;/li&gt;
&lt;li&gt;We reference the variables defined in each block in later blocks, so &lt;code&gt;micro_time&lt;/code&gt; and &lt;code&gt;tiny_time&lt;/code&gt; will need to be available &lt;em&gt;outside&lt;/em&gt; that function&lt;/li&gt;
&lt;li&gt;We need this function to run just one time right after it&amp;rsquo;s defined&amp;hellip; could we do that automatically?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All 3 of these problems are solvable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We need a decorator that accepts arguments&lt;/li&gt;
&lt;li&gt;We need to use &lt;em&gt;rarely seen&lt;/em&gt; &lt;a href="https://stackoverflow.com/a/1261961/98187"&gt;&lt;code&gt;nonlocal&lt;/code&gt;&lt;/a&gt; statement&lt;/li&gt;
&lt;li&gt;We could have the decorator automatically call the decorated function&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The final &lt;em&gt;weird&lt;/em&gt; decorator&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the decorator I ended up with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;attempt_n_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Run tests multiple times if assertions are raised.&lt;/span&gt;

&lt;span class="sd"&gt;    Allows for more forgiving tests when assertions may be a bit flaky.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;This looks like a decorator, but it actually runs the function!&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This decorator accepts an &lt;code&gt;n&lt;/code&gt; argument which determines the maximum number of times the decorated function should be called.
The decorator then &lt;em&gt;calls&lt;/em&gt; the function repeatedly in a &lt;code&gt;for&lt;/code&gt; loop and a &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt; block.
As soon as an &lt;code&gt;AssertionError&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; raised during one of these function calls, the looping stops.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;weirdest&lt;/em&gt; part about this decorator is that it calls the decorated function.
Note that the &lt;code&gt;decorator&lt;/code&gt; function doesn&amp;rsquo;t define a &lt;code&gt;wrapper&lt;/code&gt; function within itself&amp;hellip; it just runs code right away!&lt;/p&gt;
&lt;h2&gt;The resulting beautiful Python monstrosity&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the final refactored test code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_some_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.04&lt;/span&gt;
    &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;medium_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nd"&gt;@attempt_n_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;
        &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@attempt_n_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt;
        &lt;span class="n"&gt;small_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@attempt_n_times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;medium_time&lt;/span&gt;
        &lt;span class="n"&gt;medium_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;attempt_n_times&lt;/code&gt; decorator &lt;strong&gt;immediately calls the function it decorates&lt;/strong&gt;.
Each function is defined and immediately called one or more times, in a &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt; block within a loop.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why we&amp;rsquo;ve named these functions with the &lt;a href="https://stackoverflow.com/questions/36315309/how-does-python-throw-away-variable-work"&gt;throwaway&lt;/a&gt; &lt;code&gt;_&lt;/code&gt; name: &lt;strong&gt;we don&amp;rsquo;t care about the name of a function we&amp;rsquo;re never going to refer to again&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Also note the use of the &lt;code&gt;nonlocal&lt;/code&gt; statement.
Each function in Python has its own scope and all assignments &lt;a href="https://www.pythonmorsels.com/local-and-global-variables/#assigning-to-local-and-global-variables"&gt;assign to the local scope&lt;/a&gt; by default.
That &lt;code&gt;nonlocal&lt;/code&gt; variable pulls those variables to the scope of the outer function instead.&lt;/p&gt;
&lt;p&gt;Compare the above code to the code just before this refactor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_some_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.04&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;micro_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micro_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tiny_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;small_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;medium_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;micro_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiny_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;medium_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;small_time&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempts_left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I find the refactored version easier to skim.&lt;/p&gt;
&lt;p&gt;But that &lt;code&gt;attempt_n_times&lt;/code&gt; decorator &lt;em&gt;does&lt;/em&gt; abuse the decorator syntax.
Decorators aren&amp;rsquo;t &lt;em&gt;meant&lt;/em&gt; to call the function they&amp;rsquo;re decorating.&lt;/p&gt;
&lt;p&gt;Is this misuse of decorators worth it?&lt;/p&gt;
&lt;h2&gt;Is this worth it?&lt;/h2&gt;
&lt;p&gt;Decorators aren&amp;rsquo;t supposed to immediately call the function they decorate.
But there&amp;rsquo;s nothing stopping them from doing so.
I feel that I&amp;rsquo;ve traded &amp;ldquo;normal code&amp;rdquo; for a beautiful monstrosity that&amp;rsquo;s easier to skim at a glance.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;attempt_n_times&lt;/code&gt; decorator is pretending that it&amp;rsquo;s a block-level tool by using a function because there&amp;rsquo;s no other way to invent such a tool in Python.&lt;/p&gt;
&lt;p&gt;I think abstracting away the &lt;code&gt;for&lt;/code&gt;-&lt;code&gt;try&lt;/code&gt;-&lt;code&gt;break&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;if&lt;/code&gt;-&lt;code&gt;raise&lt;/code&gt; pattern was worth it, even though I ended up abusing Python&amp;rsquo;s decorator syntax in the process.&lt;/p&gt;
&lt;p&gt;What do you think?
Was that &lt;code&gt;attempt_n_times&lt;/code&gt; abstraction worth it?&lt;/p&gt;</content><category term="python"/></entry><entry><title>PyCon 2024 Reflection</title><link href="https://treyhunner.com/2024/05/pycon-2024-reflection/" rel="alternate"/><published>2024-05-28T13:00:00-07:00</published><updated>2024-05-28T13:00:00-07:00</updated><author><name>Trey Hunner</name></author><id>https://treyhunner.com/2024/05/pycon-2024-reflection</id><content type="html">&lt;p&gt;I traveled back home from PyCon US 2024 last week.
This is my reflection on my time at PyCon.&lt;/p&gt;
&lt;h2&gt;Attempting to eat vegan&lt;/h2&gt;
&lt;p&gt;Since 2020, I&amp;rsquo;ve been &lt;a href="https://mastodon.social/@treyhunner/111794737871397453"&gt;gradually eating more plant-based&lt;/a&gt; and &lt;a href="https://mastodon.social/@treyhunner/111982459215543497"&gt;a few months ago&lt;/a&gt; I decided to take PyCon as an opportunity to attempt exclusively vegan eating outside my own home.
As &lt;a href="https://mastodon.social/@treyhunner/112493037289419028"&gt;I noted on Mastodon&lt;/a&gt;, it was a challenge and I failed every day at least once but I found the experience worthwhile.
Our food system is &lt;em&gt;very&lt;/em&gt; dairy-oriented.&lt;/p&gt;
&lt;h2&gt;Staying hydrated and fed&lt;/h2&gt;
&lt;p&gt;One of the first things I did before heading to the convention center was walk to Target and buy snacks and drinks.
When at PyCon, I prefer to spend 30 minutes and $20 to have a backup plan for last minute hydration and calories (even if not the &lt;em&gt;greatest&lt;/em&gt; calories).
I never quite know when I might sleep through breakfast, find lunch lacking, or wish I&amp;rsquo;d eaten more dinner.&lt;/p&gt;
&lt;h2&gt;A tutorial, an orientation, a lightning talk, and open spaces&lt;/h2&gt;
&lt;p&gt;My responsibilities at PyCon this year included teaching a tutorial and helping run the Newcomer&amp;rsquo;s Orientation with &lt;a href="https://x.com/KojoIdrissa"&gt;Kojo&lt;/a&gt; and &lt;a href="https://social.coop/@brainwane"&gt;Sumana&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mastodon.social/@treyhunner/112479031063220835"&gt;Yngve and Marie&lt;/a&gt; offered to act as teaching assistants during my tutorial and I was very grateful for their help!
&lt;a href="https://x.com/mathsppblog"&gt;Rodrigo&lt;/a&gt; and Krishna also offered to TA just before my tutorial started and I was extra grateful to have even more help than I&amp;rsquo;d expected.
The attendees were mostly better prepared than I expected they would be, which was also great.
It&amp;rsquo;s always great to spend less time on setup and more time exploring Python together.&lt;/p&gt;
&lt;p&gt;The newcomer&amp;rsquo;s orientation the next day went well.
We kept it fairly brief and were able to address about 10 minutes of audience questions before the opening reception started.&lt;/p&gt;
&lt;p&gt;Once my PyCon responsibilities completed, I invented a few more (light) responsibilities for myself. 😅
I signed up to give a lightning talk on how to give a lightning talk.
They slotted it as the first talk of the first lightning talk session on Friday night.
I kept this talk pretty much the same as the one &lt;a href="https://youtu.be/aNHBr7q-KVw?feature=shared&amp;amp;t=915"&gt;I presented DjangoCon 2016&lt;/a&gt;.
I could have made the transitions fancier, but I decided to embrace the idea of simplicity with the hope that audience members might think &amp;ldquo;look if that first speaker can give such a simple and succinct presentation, maybe I can too.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;On Saturday I ran &lt;a href="https://mastodon.social/@treyhunner/112457077109815019"&gt;an open space on Python Learning&lt;/a&gt;.
Some of you showed up because you&amp;rsquo;re on my mailing list or you&amp;rsquo;re paying Python Morsels subscribers.
Many folks showed up because the topic was interesting, either as a learner or as a teacher.
I really enjoyed the round-table-style conversation we had.&lt;/p&gt;
&lt;p&gt;I also ran a &lt;a href="https://mastodon.social/@treyhunner/112468136603503893"&gt;Cabo Card game open space&lt;/a&gt; during lunch on Sunday on the 4th floor rooftop.
Cabo is my usual conference ice breaker game and I played it at least a few nights &lt;a href="https://mas.to/@davidism/112465501797531611"&gt;in The Westin lobby&lt;/a&gt; as well.&lt;/p&gt;
&lt;h2&gt;Seeing conference friends, old and new&lt;/h2&gt;
&lt;p&gt;For me, PyCon is largely about having conversations.
The talks and tutorials are great for starting me thinking about an idea.
The hallway track, open spaces, and meals are great for continuing conversations about those ideas (or &lt;em&gt;other&lt;/em&gt; ideas).&lt;/p&gt;
&lt;p&gt;My first morning in Pittsburgh, I chatted with &lt;a href="https://www.linkedin.com/in/naomiceder/"&gt;Naomi&lt;/a&gt; and &lt;a href="https://x.com/reuvenmlerner/"&gt;Reuven&lt;/a&gt;.
I&amp;rsquo;m glad I ran into them before the conference kicked off because (as often happens at PyCon) I only very briefly saw either of them during the rest of PyCon!&lt;/p&gt;
&lt;p&gt;After my tutorial that afternoon, I did dinner with Marie, Yngve, and Rodrigo at Rosewater Mediterranean (good vegan options, assuming you enjoy falafel and various sauces).
As sometimes happens at PyCon, another PyCon attendee, Sachin, joined our table because we noticed him eating on his own at a table near us and invited him to join us.&lt;/p&gt;
&lt;p&gt;On Saturday, &lt;a href="https://hachyderm.io/@melaniearbor"&gt;Melanie&lt;/a&gt;, &lt;a href="https://mas.to/@davidism"&gt;David&lt;/a&gt;, &lt;a href="https://x.com/kjaymiller"&gt;Jay&lt;/a&gt;, and I had a sort of mini San Diego Python study group reunion dinner before inviting folks to join us for &lt;a href="https://mastodon.social/@treyhunner/112465503888730383"&gt;Cabo and Knucklebones&lt;/a&gt; one night.
The 4 of us originally met each other (along with &lt;a href="https://hachyderm.io/@willingc"&gt;Carol&lt;/a&gt; and other wonderful Python folks) at the San Diego Python study group about 10 years ago.&lt;/p&gt;
&lt;p&gt;I had some wonderful conversations about ways to improve the Python documentation over dinner (at Nicky&amp;rsquo;s Thai) on Sunday night with &lt;em&gt;so&lt;/em&gt; many docs-concerned folks who I highly respect.
I&amp;rsquo;m really excited that Python has &lt;a href="https://peps.python.org/pep-0732/"&gt;the documentation editorial board&lt;/a&gt; and I&amp;rsquo;m hopeful that that board, with the help of many others community members, will usher in big improvements to the documentation in the coming years.&lt;/p&gt;
&lt;p&gt;I also met a number of Internet acquaintances IRL for the first time at PyCon.
I met &lt;a href="https://www.linkedin.com/in/tereza-iofciu/"&gt;Tereza&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/jessica0greene/"&gt;Jessica&lt;/a&gt;, who I know from our work in the PSF Code of Conduct workgroup.
I met &lt;a href="https://fosstodon.org/@slott56"&gt;Steve Lott&lt;/a&gt;, who I originally knew as &lt;a href="https://stackoverflow.com/users/10661/s-lott"&gt;a prolific question-answerer&lt;/a&gt;.
I also met &lt;a href="https://github.com/hugovk"&gt;Hugo&lt;/a&gt;, a CPython core dev, the Python 3.14 &amp;amp; 3.15 release manager, and a &lt;a href="https://mastodon.social/@hugovk"&gt;social media user&lt;/a&gt; (which is how I&amp;rsquo;ve primarily interacted with him because the Internet is occasionally lovely).
I was also very excited to meet many &lt;a href="https://www.pythonmorsels.com"&gt;Python Morsels members&lt;/a&gt; as well as folks who know me through &lt;a href="https://www.pythonmorsels.com/newsletter/"&gt;my weekly Python tips newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was grateful to chat with &lt;a href="https://mastodon.social/@hynek"&gt;Hynek&lt;/a&gt; and &lt;a href="https://mastodon.social/@AlSweigart"&gt;Al&lt;/a&gt; about creating talks, YouTube videos, and other online content.
I also enjoyed chatting with &lt;a href="https://mastodon.social/@glyph"&gt;Glyph&lt;/a&gt; a bit about our experiences consulting and training and (in hindsight) wished I&amp;rsquo;d planned an open space for either consultants or trainers, both of which have been held at PyCon before but it just takes someone to stick it on the open space board.&lt;/p&gt;
&lt;p&gt;Many folks I only saw very briefly (I said a quick hi and bye to &lt;a href="https://aeracode.org/@andrew"&gt;Andrew&lt;/a&gt; over lunch during the sprints) and some I didn&amp;rsquo;t see at all (&lt;a href="https://mastodon.social/@frank@frankwiles.social"&gt;Frank&lt;/a&gt; was at PyCon but we never ran into each other).
Some I essentially saw through playing a few rounds of Cabo (&lt;a href="https://social.coop/@Yhg1s"&gt;Thomas&lt;/a&gt; and &lt;a href="https://hachyderm.io/@ethantyping"&gt;Ethan&lt;/a&gt; among many others).
We also ran into at least 4 other PyCon attendees in the airport on Tuesday afternoon, including &lt;a href="https://www.linkedin.com/in/bbelderbos/"&gt;Bob&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/juliansequeira/"&gt;Julian&lt;/a&gt;, who it&amp;rsquo;s always a pleasure to see.&lt;/p&gt;
&lt;h2&gt;A Mastodon-oriented PyCon&lt;/h2&gt;
&lt;p&gt;On Thursday night I had the feeling that the number of Mastodon posts I saw on the &lt;strong&gt;#PyConUS&lt;/strong&gt; hashtag was greater than the number of Twitter posts.
I (very unscientifically) counted up the number of posts I was seeing on each and found that my perception was correct: &lt;a href="https://mastodon.social/@treyhunner/112453920848761679"&gt;Mastodon seemed to slightly overtake Twitter at PyCon this year&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Over dinner on Wednesday, I tried to convince Marie, Yngve, and Rodrigo to get Mastodon accounts just to follow the hashtag during PyCon.
I succeeded: &lt;a href="https://mastodon.social/@treyhunner/112479031063220835"&gt;Marie and Yngve&lt;/a&gt; and &lt;a href="https://fosstodon.org/@davep/112458736212760528"&gt;Rodrigo&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Mastodon will never be &lt;em&gt;the&lt;/em&gt; social media platform.
Its decentralized nature is too much of a barrier for many folks.
However, it does seem to be used by &lt;em&gt;enough&lt;/em&gt; somewhat nerdy Python folks to now be one the most used social media platform for PyCon posting.&lt;/p&gt;
&lt;h2&gt;The talks&lt;/h2&gt;
&lt;p&gt;I ended up spending little time in the talks during PyCon.
This wasn&amp;rsquo;t on purpose.
I just happened to attend many open spaces, take personal breaks, and end up in hallway conversations often.
I did see many of the lightning talks live, as well as &lt;a href="https://x.com/kjaymiller"&gt;Jay&lt;/a&gt;, &lt;a href="https://simonwillison.net/@simon"&gt;Simon&lt;/a&gt;, and &lt;a href="https://social.coop/@brainwane"&gt;Sumana&lt;/a&gt;&amp;rsquo;s keynotes (all of them were exceptional) and the opening and closing remarks.
I also watched a few talks from my hotel room while taking breaks.&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;m often a bit light on my talk load at PyCon, I do recommend folks attend a good handful of live talks during PyCon, &lt;a href="https://mastodon.social/@jonafato/112514979873634457"&gt;as Jon&lt;/a&gt; and &lt;a href="https://hynek.me/articles/hallway-track/"&gt;others&lt;/a&gt; recommend.
I wish I had seen more talks live.
I also wish I had attended a few open spaces that I missed.&lt;/p&gt;
&lt;p&gt;At any one time, I know that I&amp;rsquo;m always missing about 90% of what&amp;rsquo;s scheduled during PyCon (if you include the talks &lt;em&gt;and&lt;/em&gt; the open spaces).
That&amp;rsquo;s assuming I don&amp;rsquo;t ditch the conference entirely for a few hours and &lt;a href="https://social.coop/@bitprophet/112452662184950234"&gt;walk across a bridge&lt;/a&gt; or &lt;a href="https://mastodon.social/@AlSweigart/112514009252817862"&gt;ride a funicular&lt;/a&gt; (neither of which I did, as I stuck around the venue the whole time this year).
I am glad I saw, did, and talked about everything I did, but there&amp;rsquo;s always something I wish I&amp;rsquo;d seen/done!&lt;/p&gt;
&lt;h2&gt;The sprints&lt;/h2&gt;
&lt;p&gt;Thanks to the documentation dinner, I had a couple documentation-related ideas in mind on the first day of sprints.
But I&amp;rsquo;m also &lt;em&gt;really&lt;/em&gt; excited about the new Python REPL coming in Python 3.13 (&lt;a href="https://x.com/treyhunner/status/1720185049461801371"&gt;in case&lt;/a&gt; you &lt;a href="https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv/"&gt;can&amp;rsquo;t tell&lt;/a&gt; from &lt;a href="https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature/"&gt;how much&lt;/a&gt; I &lt;a href="https://x.com/treyhunner/status/1788307498715554160"&gt;talk&lt;/a&gt; about &lt;a href="https://www.linkedin.com/posts/treyhunner_python-activity-7194082747315859456-qQjk/?utm_source=share&amp;amp;utm_medium=member_desktop"&gt;it&lt;/a&gt;), so I sprinted on that instead.
&lt;a href="https://github.com/ambv"&gt;Łukasz&lt;/a&gt; assigned me the task of researching keyboard shortcuts that the new REPL is missing (compared to the current one on Linux and Mac) so &lt;a href="https://github.com/python/cpython/issues/119034#issuecomment-2121142576"&gt;I spent some time researching that&lt;/a&gt;.
I got to see the REPL running on &lt;a href="https://fosstodon.org/@tonybaloney/112477098396842900"&gt;Anthony&amp;rsquo;s laptop&lt;/a&gt; on Windows and I am &lt;em&gt;so excited&lt;/em&gt; that Windows support will be included before 3.13.0 lands! 🎉&lt;/p&gt;
&lt;p&gt;Partly inspired by &lt;a href="https://youtu.be/RL3HFj5SDqI?t=1549"&gt;Carol Willing&amp;rsquo;s PyCon preview message&lt;/a&gt;, I also thanked &lt;a href="https://github.com/pablogsal"&gt;Pablo&lt;/a&gt;, &lt;a href="https://github.com/ambv"&gt;Łukasz&lt;/a&gt;, and &lt;a href="https://github.com/lysnikolaou"&gt;Lysandros&lt;/a&gt; in-person for all their work on the new Python REPL. 🤗&lt;/p&gt;
&lt;h2&gt;Until next year&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ll be &lt;a href="https://www.pyohio.org/2024/program/speakers/keynote-speakers/#trey-hunner"&gt;keynoting at PyOhio&lt;/a&gt; this year.&lt;/p&gt;
&lt;p&gt;Besides PyOhio, I&amp;rsquo;m not sure whether I&amp;rsquo;ll make it to another conference until PyCon US next year.
I&amp;rsquo;d love to attend all of them, but I do have work and personal goals that need accomplishing too!&lt;/p&gt;
&lt;p&gt;I hope to see you at PyCon US 2025!
In the meantime, if you&amp;rsquo;re wishing we&amp;rsquo;d exchanged contact details or met in-person, please feel free to stay in touch through &lt;a href="https://mastodon.social/@treyhunner"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://mastodon.social/@treyhunner"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://pym.dev/newsletter"&gt;my weekly emails&lt;/a&gt;, &lt;a href="https://www.youtube.com/@PythonMorsels"&gt;YouTube&lt;/a&gt;, or &lt;a href="http://twitter.com/treyhunner"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Yellow bridges over a river just outside the PyCon 2024 conference center in Pittsburgh" class="no-border" src="/images/pycon2024-pic.jpg"&gt;&lt;/p&gt;</content><category term="python"/><category term="pycon"/></entry></feed>