|
1252 | 1252 | 'prose': 'Use `is` and `id()` to observe identity while two names refer to the ' |
1253 | 1253 | 'same object.'}]}, |
1254 | 1254 | {'cells': [{'code': 'english = "hello"\n' |
| 1255 | + 'french = "café"\n' |
1255 | 1256 | 'thai = "สวัสดี"\n' |
1256 | 1257 | '\n' |
1257 | | - 'for label, word in [("English", english), ("Thai", thai)]:\n' |
| 1258 | + 'for label, word in [("English", english), ("French", french), ("Thai", ' |
| 1259 | + 'thai)]:\n' |
1258 | 1260 | ' print(label, word, len(word), len(word.encode("utf-8")))', |
1259 | 1261 | 'kind': 'cell', |
1260 | 1262 | 'line': 17, |
1261 | | - 'output': 'English hello 5 5\nThai สวัสดี 6 18', |
1262 | | - 'prose': ['Compare an English greeting with a Thai greeting. Both are Python `str` ' |
1263 | | - 'values, but UTF-8 uses one byte for each ASCII code point and multiple ' |
1264 | | - 'bytes for many non-ASCII code points.']}, |
| 1263 | + 'output': 'English hello 5 5\nFrench café 4 5\nThai สวัสดี 6 18', |
| 1264 | + 'prose': ['Compare three words by code-point count and UTF-8 byte count. ASCII ' |
| 1265 | + 'characters take one byte each (`hello` → 5 bytes); the `é` in `café` is ' |
| 1266 | + 'one code point but two UTF-8 bytes; each Thai character takes three. The ' |
| 1267 | + '`str` type abstracts over all three.']}, |
1265 | 1268 | {'code': 'print(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])', |
1266 | 1269 | 'kind': 'cell', |
1267 | | - 'line': 34, |
| 1270 | + 'line': 36, |
1268 | 1271 | 'output': "ส\n['0xe2a', '0xe27']", |
1269 | 1272 | 'prose': ['Indexing and iteration work with Unicode code points, not encoded bytes. ' |
1270 | 1273 | '`ord()` returns the integer code point, which is often displayed in ' |
|
1275 | 1278 | 'print(clean.upper())\n' |
1276 | 1279 | 'print(clean.encode("utf-8"))', |
1277 | 1280 | 'kind': 'cell', |
1278 | | - 'line': 48, |
| 1281 | + 'line': 50, |
1279 | 1282 | 'output': "café\nCAFÉ\nb'caf\\xc3\\xa9'", |
1280 | 1283 | 'prose': ['String methods return new strings because strings are immutable. Encoding ' |
1281 | 1284 | 'turns text into bytes when another system needs a byte representation.']}], |
1282 | 1285 | 'code': 'english = "hello"\n' |
| 1286 | + 'french = "café"\n' |
1283 | 1287 | 'thai = "สวัสดี"\n' |
1284 | 1288 | '\n' |
1285 | | - 'for label, word in [("English", english), ("Thai", thai)]:\n' |
| 1289 | + 'for label, word in [("English", english), ("French", french), ("Thai", thai)]:\n' |
1286 | 1290 | ' print(label, word, len(word), len(word.encode("utf-8")))\n' |
1287 | 1291 | '\n' |
1288 | 1292 | 'print(thai[0])\n' |
|
1296 | 1300 | 'doc_path': '/library/stdtypes.html#text-sequence-type-str', |
1297 | 1301 | 'doc_url': 'https://docs.python.org/3.13/library/stdtypes.html#text-sequence-type-str', |
1298 | 1302 | 'expected_output': 'English hello 5 5\n' |
| 1303 | + 'French café 4 5\n' |
1299 | 1304 | 'Thai สวัสดี 6 18\n' |
1300 | 1305 | 'ส\n' |
1301 | 1306 | "['0xe2a', '0xe27']\n" |
|
1328 | 1333 | 'version_notes': None, |
1329 | 1334 | 'version_sensitive': False, |
1330 | 1335 | 'walkthrough': [{'code': 'english = "hello"\n' |
| 1336 | + 'french = "café"\n' |
1331 | 1337 | 'thai = "สวัสดี"\n' |
1332 | 1338 | '\n' |
1333 | | - 'for label, word in [("English", english), ("Thai", thai)]:\n' |
| 1339 | + 'for label, word in [("English", english), ("French", french), ("Thai", ' |
| 1340 | + 'thai)]:\n' |
1334 | 1341 | ' print(label, word, len(word), len(word.encode("utf-8")))', |
1335 | | - 'prose': 'Compare an English greeting with a Thai greeting. Both are Python ' |
1336 | | - '`str` values, but UTF-8 uses one byte for each ASCII code point and ' |
1337 | | - 'multiple bytes for many non-ASCII code points.'}, |
| 1342 | + 'prose': 'Compare three words by code-point count and UTF-8 byte count. ASCII ' |
| 1343 | + 'characters take one byte each (`hello` → 5 bytes); the `é` in `café` ' |
| 1344 | + 'is one code point but two UTF-8 bytes; each Thai character takes ' |
| 1345 | + 'three. The `str` type abstracts over all three.'}, |
1338 | 1346 | {'code': 'print(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])', |
1339 | 1347 | 'prose': 'Indexing and iteration work with Unicode code points, not encoded ' |
1340 | 1348 | 'bytes. `ord()` returns the integer code point, which is often ' |
|
7679 | 7687 | 'kind': 'unsupported', |
7680 | 7688 | 'line': 18, |
7681 | 7689 | 'output': '', |
7682 | | - 'prose': ['Dynamic Workers do not provide the `venv` module or a project environment ' |
7683 | | - 'workflow.']}, |
| 7690 | + 'prose': ['`venv.EnvBuilder` configures the description of a new environment, then ' |
| 7691 | + '`create(".venv")` materialises it on disk as a directory containing its ' |
| 7692 | + 'own interpreter and `site-packages`. `with_pip=False` skips bootstrapping ' |
| 7693 | + "pip — useful when the venv is for an isolated tool that doesn't need to " |
| 7694 | + 'install third-party packages. (This fragment runs in standard Python only ' |
| 7695 | + "— Dynamic Workers don't provide the `venv` module or a project environment " |
| 7696 | + 'workflow.)']}, |
7684 | 7697 | {'code': 'import pathlib\n' |
7685 | 7698 | 'import tempfile\n' |
7686 | 7699 | 'import venv\n' |
|
9812 | 9825 | 'logger = logging.getLogger("example")\n' |
9813 | 9826 | 'logger.setLevel(logging.INFO)\n' |
9814 | 9827 | 'handler = logging.StreamHandler(sys.stdout)\n' |
9815 | | - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' |
| 9828 | + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' |
| 9829 | + 'handler.setFormatter(formatter)\n' |
9816 | 9830 | 'logger.handlers[:] = [handler]\n' |
9817 | 9831 | '\n' |
9818 | 9832 | 'logger.debug("hidden")\n' |
|
9828 | 9842 | 'logger = logging.getLogger("example")\n' |
9829 | 9843 | 'logger.setLevel(logging.INFO)\n' |
9830 | 9844 | 'handler = logging.StreamHandler(sys.stdout)\n' |
9831 | | - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' |
| 9845 | + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' |
| 9846 | + 'handler.setFormatter(formatter)\n' |
9832 | 9847 | 'logger.handlers[:] = [handler]\n' |
9833 | 9848 | '\n' |
9834 | 9849 | 'logger.debug("hidden")\n' |
|
9864 | 9879 | 'logger = logging.getLogger("example")\n' |
9865 | 9880 | 'logger.setLevel(logging.INFO)\n' |
9866 | 9881 | 'handler = logging.StreamHandler(sys.stdout)\n' |
9867 | | - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' |
| 9882 | + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' |
| 9883 | + 'handler.setFormatter(formatter)\n' |
9868 | 9884 | 'logger.handlers[:] = [handler]\n' |
9869 | 9885 | '\n' |
9870 | 9886 | 'logger.debug("hidden")\n' |
|
9911 | 9927 | '`assertRaises` asserts that a block raises the expected exception type.']}, |
9912 | 9928 | {'code': 'import io\n' |
9913 | 9929 | '\n' |
9914 | | - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' |
| 9930 | + 'loader = unittest.defaultTestLoader\n' |
| 9931 | + 'suite = loader.loadTestsFromTestCase(AddTests)\n' |
9915 | 9932 | 'stream = io.StringIO()\n' |
9916 | | - 'result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\n' |
| 9933 | + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' |
| 9934 | + 'result = runner.run(suite)\n' |
9917 | 9935 | 'print(result.testsRun)\n' |
9918 | 9936 | 'print(result.wasSuccessful())', |
9919 | 9937 | 'kind': 'cell', |
|
9949 | 9967 | ' with self.assertRaises(ZeroDivisionError):\n' |
9950 | 9968 | ' divide(1, 0)\n' |
9951 | 9969 | '\n' |
9952 | | - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' |
| 9970 | + 'loader = unittest.defaultTestLoader\n' |
| 9971 | + 'suite = loader.loadTestsFromTestCase(AddTests)\n' |
9953 | 9972 | 'stream = io.StringIO()\n' |
9954 | | - 'result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\n' |
| 9973 | + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' |
| 9974 | + 'result = runner.run(suite)\n' |
9955 | 9975 | 'print(result.testsRun)\n' |
9956 | 9976 | 'print(result.wasSuccessful())\n', |
9957 | 9977 | 'doc_path': '/library/unittest.html', |
@@ -10011,10 +10031,11 @@ |
10011 | 10031 | 'type.'}, |
10012 | 10032 | {'code': 'import io\n' |
10013 | 10033 | '\n' |
10014 | | - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' |
| 10034 | + 'loader = unittest.defaultTestLoader\n' |
| 10035 | + 'suite = loader.loadTestsFromTestCase(AddTests)\n' |
10015 | 10036 | 'stream = io.StringIO()\n' |
10016 | | - 'result = unittest.TextTestRunner(stream=stream, ' |
10017 | | - 'verbosity=0).run(suite)\n' |
| 10037 | + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' |
| 10038 | + 'result = runner.run(suite)\n' |
10018 | 10039 | 'print(result.testsRun)\n' |
10019 | 10040 | 'print(result.wasSuccessful())', |
10020 | 10041 | 'prose': 'A runner executes the suite and records whether every assertion ' |
|
10029 | 10050 | 'kind': 'unsupported', |
10030 | 10051 | 'line': 18, |
10031 | 10052 | 'output': '', |
10032 | | - 'prose': ['Dynamic Workers do not provide child processes.']}, |
| 10053 | + 'prose': ['`subprocess.run` spawns a child Python interpreter, captures its stdout ' |
| 10054 | + 'and stderr (`capture_output=True`), decodes them as text (`text=True`), ' |
| 10055 | + 'and raises `CalledProcessError` if the child exits non-zero ' |
| 10056 | + '(`check=True`). The returned `result` holds the captured streams and exit ' |
| 10057 | + 'code as portable evidence the child ran. (This fragment runs in standard ' |
| 10058 | + "Python only — Dynamic Workers don't provide child processes.)"]}, |
10033 | 10059 | {'code': 'import subprocess\n' |
10034 | 10060 | 'import sys\n' |
10035 | 10061 | '\n' |
|
10108 | 10134 | 'kind': 'unsupported', |
10109 | 10135 | 'line': 18, |
10110 | 10136 | 'output': '', |
10111 | | - 'prose': ['Dynamic Workers do not provide native threads or child processes.']}, |
| 10137 | + 'prose': ['`ThreadPoolExecutor` runs `square` across two worker threads sharing the ' |
| 10138 | + 'same interpreter (and the GIL); `ProcessPoolExecutor` runs `pow` across ' |
| 10139 | + 'two child processes with isolated memory. Each `pool.map` returns an ' |
| 10140 | + 'iterator over results in input order, and the surrounding `with` block ' |
| 10141 | + 'joins the workers when the body exits. (This fragment runs in standard ' |
| 10142 | + "Python only — Dynamic Workers don't provide native threads or child " |
| 10143 | + 'processes.)']}, |
10112 | 10144 | {'code': 'from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n' |
10113 | 10145 | '\n' |
10114 | 10146 | '\n' |
|
10184 | 10216 | 'kind': 'unsupported', |
10185 | 10217 | 'line': 18, |
10186 | 10218 | 'output': '', |
10187 | | - 'prose': ['Dynamic Workers do not provide arbitrary low-level sockets, and this app ' |
10188 | | - 'disables Dynamic Worker outbound access.']}, |
| 10219 | + 'prose': ['`socketpair()` returns two connected endpoints. `sendall` writes encoded ' |
| 10220 | + 'bytes into one end, and `recv` reads up to 16 bytes off the other. The ' |
| 10221 | + 'byte boundary is the whole point: `"ping".encode("utf-8")` produces ' |
| 10222 | + "`b'ping'`, which is what the socket actually moves. (This fragment runs in " |
| 10223 | + "standard Python only — Dynamic Workers don't expose arbitrary sockets and " |
| 10224 | + 'this app disables Worker outbound access.)']}, |
10189 | 10225 | {'code': 'import socket\n' |
10190 | 10226 | '\n' |
10191 | 10227 | 'left, right = socket.socketpair()\n' |
|
10201 | 10237 | 'kind': 'cell', |
10202 | 10238 | 'line': 28, |
10203 | 10239 | 'output': "b'ping'\nping", |
10204 | | - 'prose': ['Sockets exchange bytes. Encoding and decoding make the boundary between ' |
10205 | | - 'Python text and network data visible.']}], |
| 10240 | + 'prose': ['The complete version adds two things: a `try`/`finally` so both endpoints ' |
| 10241 | + 'close even if `recv` or the surrounding work raises, and a second `print` ' |
| 10242 | + 'that `decode`s the received bytes back into a Python `str` for display. ' |
| 10243 | + "The first `print` shows the raw bytes `b'ping'`; the second shows the " |
| 10244 | + 'decoded text `ping`.']}], |
10206 | 10245 | 'code': 'import socket\n' |
10207 | 10246 | '\n' |
10208 | 10247 | 'left, right = socket.socketpair()\n' |
|
10251 | 10290 | 'finally:\n' |
10252 | 10291 | ' left.close()\n' |
10253 | 10292 | ' right.close()', |
10254 | | - 'prose': 'Sockets exchange bytes. Encoding and decoding make the boundary ' |
10255 | | - 'between Python text and network data visible.'}]}, |
| 10293 | + 'prose': 'The complete version adds two things: a `try`/`finally` so both ' |
| 10294 | + 'endpoints close even if `recv` or the surrounding work raises, and a ' |
| 10295 | + 'second `print` that `decode`s the received bytes back into a Python ' |
| 10296 | + "`str` for display. The first `print` shows the raw bytes `b'ping'`; " |
| 10297 | + 'the second shows the decoded text `ping`.'}]}, |
10256 | 10298 | {'cells': [{'code': 'from datetime import date, datetime, time, timedelta, timezone\n' |
10257 | 10299 | '\n' |
10258 | 10300 | 'release_day = date(2026, 5, 4)\n' |
|
10280 | 10322 | 'prose': ['Use `timedelta` for durations. Adding one to a `datetime` produces another ' |
10281 | 10323 | '`datetime` without manually changing calendar fields.']}, |
10282 | 10324 | {'code': 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' |
10283 | | - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' |
| 10325 | + 'iso_text = "2026-05-04T12:30:00+00:00"\n' |
| 10326 | + 'parsed = datetime.fromisoformat(iso_text)\n' |
10284 | 10327 | 'print(parsed == created_at)', |
10285 | 10328 | 'kind': 'cell', |
10286 | 10329 | 'line': 56, |
|
10301 | 10344 | 'print(expires_at.isoformat())\n' |
10302 | 10345 | '\n' |
10303 | 10346 | 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' |
10304 | | - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' |
| 10347 | + 'iso_text = "2026-05-04T12:30:00+00:00"\n' |
| 10348 | + 'parsed = datetime.fromisoformat(iso_text)\n' |
10305 | 10349 | 'print(parsed == created_at)\n', |
10306 | 10350 | 'doc_path': '/library/datetime.html', |
10307 | 10351 | 'doc_url': 'https://docs.python.org/3.13/library/datetime.html', |
|
10371 | 10415 | 'prose': 'Use `timedelta` for durations. Adding one to a `datetime` produces ' |
10372 | 10416 | 'another `datetime` without manually changing calendar fields.'}, |
10373 | 10417 | {'code': 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' |
10374 | | - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' |
| 10418 | + 'iso_text = "2026-05-04T12:30:00+00:00"\n' |
| 10419 | + 'parsed = datetime.fromisoformat(iso_text)\n' |
10375 | 10420 | 'print(parsed == created_at)', |
10376 | 10421 | 'prose': 'Use `strftime()` for human-facing formatting and `fromisoformat()` ' |
10377 | 10422 | 'when reading ISO 8601 text back into a `datetime`.'}]}, |
|
0 commit comments