Repository navigation
Expand file tree
/
Copy pathpython-intermediate.html
More file actions
4259 lines (3791 loc) · 214 KB
/
Copy pathpython-intermediate.html
File metadata and controls
4259 lines (3791 loc) · 214 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<!--
David's Python Intermediate — interactive intermediate course (PyI badge).
Shell: shared CSS, runtime, and Pyodide v0.26.4 (offline cache shared across both courses).
Content: 44 lessons, Modules 0-8 plus appendices, quizzes qi1-qi6.
Storage namespace pyi (edits, progress, theme) — side-by-side safe with the other course.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>David's Python Intermediate — Interactive Intermediate Course</title>
<link rel="icon" type="image/svg+xml"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128' viewBox='0 0 128 128'%3E%3Crect x='3' y='3' width='122' height='122' rx='26' fill='%230d1117' stroke='%2330363d' stroke-width='4'/%3E%3Ctext x='64' y='84' font-family='DejaVu Sans, Verdana, sans-serif' font-size='58' font-weight='bold' text-anchor='middle'%3E%3Ctspan fill='%2358a6ff'%3EP%3C/tspan%3E%3Ctspan dx='-6' fill='%23f0883e'%3Ey%3C/tspan%3E%3Ctspan dx='-8' font-size='48' fill='%23e6edf3'%3EI%3C/tspan%3E%3C/text%3E%3C/svg%3E">
<style>
:root{
--bg:#0d1117; --bg2:#161b22; --bg3:#21262d; --border:#30363d;
--text:#e6edf3; --muted:#8b949e; --accent:#58a6ff; --ok:#3fb950; --warn:#d29922; --err:#f85149;
--code-bg:#0b0f14;
--kw:#ff7b72; --str:#a5d6ff; --com:#8b949e; --num:#79c0ff; --bi:#d2a8ff;
--mono:ui-monospace,"SF Mono","Cascadia Mono","JetBrains Mono",Menlo,Consolas,"DejaVu Sans Mono",monospace;
--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-theme="light"]{
--bg:#f6f8fa; --bg2:#ffffff; --bg3:#eef1f4; --border:#d0d7de;
--text:#1f2328; --muted:#57606a; --accent:#0969da; --ok:#1a7f37; --warn:#9a6700; --err:#cf222e;
--code-bg:#f6f8fa;
--kw:#cf222e; --str:#0a3069; --com:#6e7781; --num:#0550ae; --bi:#8250df;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.7 var(--sans)}
a{color:var(--accent)}
h1,h2,h3,h4{line-height:1.35}
h2{font-size:25px;margin:0 0 18px}
h2 .no{display:inline-block;font:600 12.5px var(--mono);color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);border-radius:6px;padding:2px 8px;margin-right:10px;vertical-align:2px}
h3{font-size:18.5px;margin:30px 0 10px}
h4{font-size:16px;margin:18px 0 8px}
p{margin:12px 0}
ul,ol{margin:12px 0;padding-left:26px}
li{margin:5px 0}
code{font:.88em var(--mono);background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:1px 6px}
kbd{font:600 11.5px var(--mono);border:1px solid var(--border);border-bottom-width:2px;border-radius:4px;padding:1px 6px;background:var(--bg3)}
table{border-collapse:collapse;width:100%;margin:18px 0;font-size:14.5px}
th,td{border:1px solid var(--border);padding:8px 12px;text-align:left;vertical-align:top}
th{background:var(--bg3);font-weight:600}
section.lesson{margin:0 0 54px;padding-bottom:8px;scroll-margin-top:76px}
noscript div{padding:20px;margin:20px;border:2px solid var(--err);border-radius:10px}
/* ── top bar ─────────────────────────────── */
#topbar{position:fixed;top:0;left:0;right:0;height:56px;display:flex;align-items:center;gap:10px;padding:0 16px;background:rgba(13,17,23,.94);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border-bottom:1px solid var(--border);z-index:50}
[data-theme="light"] #topbar{background:rgba(246,248,250,.94)}
#brand{font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#brand .tag{color:var(--muted);font-size:12px;margin-left:8px}
#progress-wrap{margin-left:auto;display:flex;align-items:center;gap:10px;font-size:12px;color:var(--muted);white-space:nowrap}
#progress-bar{width:110px;height:6px;border-radius:3px;background:var(--bg3);overflow:hidden}
#progress-fill{height:100%;width:0;background:linear-gradient(90deg,var(--ok),var(--accent));transition:width .3s}
.tb-btn{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 10px;font:600 12.5px var(--sans);cursor:pointer;white-space:nowrap}
.tb-btn:hover{border-color:var(--accent);color:var(--accent)}
#btn-menu{display:none}
/* ── layout ──────────────────────────────── */
#layout{display:flex;padding-top:56px;min-height:100vh}
#sidebar{width:300px;flex:0 0 300px;position:sticky;top:56px;height:calc(100vh - 56px);overflow-y:auto;padding:18px 12px 60px;border-right:1px solid var(--border);background:var(--bg2);scrollbar-width:thin}
main{flex:1;min-width:0;max-width:870px;margin:0 auto;padding:38px 44px 120px}
.side-title{font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:0 12px;margin:20px 0 6px;font-weight:700}
.side-link{display:flex;justify-content:space-between;gap:8px;padding:5px 12px;border-radius:6px;color:var(--muted);text-decoration:none;font-size:13.2px;line-height:1.4}
.side-link:hover{color:var(--text);background:var(--bg3)}
.side-link.active{color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent)}
.side-link .ck{color:var(--ok);font-size:11px;flex:0 0 14px}
@media (max-width:940px){
#btn-menu{display:inline-block}
#brand .tag{display:none}
#progress-wrap{display:none}
#sidebar{position:fixed;left:0;top:56px;bottom:0;z-index:49;transform:translateX(-102%);transition:transform .25s;box-shadow:none}
body.nav-open #sidebar{transform:none;box-shadow:12px 0 40px rgba(0,0,0,.5)}
main{padding:28px 20px 100px}
}
/* ── editor ──────────────────────────────── */
.editor,.static-code{border:1px solid var(--border);border-radius:10px;margin:20px 0;background:var(--code-bg);overflow:hidden}
.ed-head,.sc-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:7px 12px;background:var(--bg3);border-bottom:1px solid var(--border)}
.ed-title,.sc-title{font:600 12.5px var(--sans);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ed-actions{display:flex;gap:8px;flex:0 0 auto}
.ed-run{background:#238636;border:1px solid #2ea043;color:#fff;border-radius:6px;padding:4px 14px;font:600 13px var(--sans);cursor:pointer}
.ed-run:hover{background:#2ea043}
.ed-run:disabled{opacity:.55;cursor:default}
.ed-reset{background:transparent;border:1px solid var(--border);color:var(--muted);border-radius:6px;padding:4px 10px;font:600 13px var(--sans);cursor:pointer}
.ed-reset:hover{color:var(--text);border-color:var(--accent)}
.ed-body{position:relative}
.ed-ta,.ed-hl{font:400 14px/22px var(--mono);letter-spacing:0;padding:12px 14px;white-space:pre;tab-size:4;-moz-tab-size:4;font-variant-ligatures:none;overflow-wrap:normal;word-break:normal}
.ed-ta{position:absolute;inset:0;width:100%;height:100%;background:transparent;border:0;outline:0;resize:none;color:transparent;caret-color:var(--text);overflow:auto;scrollbar-width:none;z-index:2}
.ed-ta::-webkit-scrollbar{display:none}
.ed-ta::selection{background:rgba(88,166,255,.30)}
.ed-hl{position:absolute;inset:0;margin:0;overflow:hidden;pointer-events:none;border:0;z-index:1}
.ed-hl code{display:block;font:inherit;white-space:pre;padding:0;border:0;background:transparent}
.ed-out{margin:0;padding:10px 14px;background:color-mix(in srgb,var(--code-bg) 60%,#000);border-top:1px solid var(--border);min-height:22px;max-height:340px;overflow:auto;font:13px/1.55 var(--mono);white-space:pre-wrap;word-break:break-word;scrollbar-width:thin}
.ed-out .err{color:#ff7b72}
.ed-out .dim{color:var(--muted);font-style:italic}
.ed-meta{padding:6px 14px;font-size:11.5px;color:var(--muted);border-top:1px dashed var(--border)}
.static-code .sc-body{margin:0;padding:12px 14px;overflow:auto;font:13.5px/1.6 var(--mono);white-space:pre;scrollbar-width:thin}
.static-code .sc-body.plain{color:var(--muted)}
.tok-kw{color:var(--kw)}.tok-str{color:var(--str)}.tok-com{color:var(--com);font-style:italic}.tok-num{color:var(--num)}.tok-bi{color:var(--bi)}
/* ── callouts ────────────────────────────── */
.callout{border-left:3px solid var(--accent);background:color-mix(in srgb,var(--accent) 8%,transparent);padding:12px 18px;margin:18px 0;border-radius:0 8px 8px 0;font-size:15px}
.callout .t{font-weight:700;display:block;margin-bottom:4px}
.callout.why{border-color:var(--accent)}
.callout.tip{border-color:var(--ok);background:color-mix(in srgb,var(--ok) 8%,transparent)}
.callout.warn{border-color:var(--warn);background:color-mix(in srgb,var(--warn) 9%,transparent)}
.callout.ana{border-color:#bc8cff;background:color-mix(in srgb,#bc8cff 9%,transparent)}
.callout.crash{border-color:var(--err);background:color-mix(in srgb,var(--err) 8%,transparent)}
.callout ul{margin:8px 0 2px}
/* ── exercises / solutions ───────────────── */
.exercise{border:1px solid color-mix(in srgb,var(--ok) 45%,transparent);border-radius:12px;padding:16px 20px 6px;margin:24px 0;background:color-mix(in srgb,var(--ok) 4%,transparent)}
.exercise>h4{margin-top:0}
details.hint,details.solution{border:1px solid var(--border);border-radius:10px;margin:12px 0 18px;background:var(--bg2)}
details.hint>summary,details.solution>summary{cursor:pointer;padding:10px 16px;color:var(--muted);font:600 13.5px var(--sans);user-select:none}
details.hint[open]>summary,details.solution[open]>summary{border-bottom:1px solid var(--border)}
details.hint>div,details.solution>div{padding:8px 16px 14px}
/* ── quiz ────────────────────────────────── */
.quiz{border:1px solid var(--border);border-radius:12px;background:var(--bg2);padding:18px 22px;margin:24px 0}
.quiz-title{font-weight:700;margin-bottom:8px}
.q{margin:14px 0;padding:12px 0 4px;border-bottom:1px dashed var(--border);border-bottom-style:dashed}
.q .qt{font-weight:600;margin-bottom:8px}
.q label{display:flex;gap:10px;padding:7px 12px;border:1px solid var(--border);border-radius:8px;margin:6px 0;cursor:pointer;align-items:flex-start}
.q label:hover{border-color:var(--accent)}
.q label.chosen{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,transparent)}
.q.checked label{cursor:default}
.q.checked label.correct{border-color:var(--ok);background:color-mix(in srgb,var(--ok) 10%,transparent)}
.q.checked label.wrong{border-color:var(--err);background:color-mix(in srgb,var(--err) 8%,transparent)}
.q input[type=radio]{accent-color:var(--accent);margin-top:4px}
.q .exp{display:none;color:var(--muted);font-size:14px;margin:10px 0 6px}
.q.checked .exp{display:block}
.quiz-bar{display:flex;gap:10px;margin-top:14px}
.quiz-score{margin-top:12px;font-weight:600}
/* ── lesson footer ───────────────────────── */
.lesson-foot{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:30px;padding-top:16px;border-top:1px solid var(--border)}
.complete-btn{background:transparent;border:1px solid var(--border);color:var(--muted);border-radius:6px;padding:6px 12px;font:600 12.5px var(--sans);cursor:pointer}
.complete-btn:hover{border-color:var(--ok);color:var(--ok)}
.complete-btn.done{border-color:var(--ok);color:var(--ok);background:color-mix(in srgb,var(--ok) 10%,transparent)}
.pn{display:flex;gap:10px}
.pn a{text-decoration:none;font:600 12.5px var(--sans);border:1px solid var(--border);color:var(--muted);border-radius:6px;padding:6px 12px}
.pn a:hover{color:var(--accent);border-color:var(--accent)}
/* ── REPL ────────────────────────────────── */
#repl{position:fixed;left:0;right:0;bottom:0;height:min(360px,55vh);background:var(--bg2);border-top:2px solid var(--accent);transform:translateY(105%);transition:transform .25s;z-index:60;display:flex;flex-direction:column;box-shadow:0 -10px 40px rgba(0,0,0,.5)}
#repl.open{transform:none}
.repl-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:9px 18px;border-bottom:1px solid var(--border);font:600 13.5px var(--sans)}
.repl-head .hintx{color:var(--muted);font:400 12px var(--sans)}
#repl-close{background:none;border:0;color:var(--muted);font-size:18px;cursor:pointer;line-height:1}
#repl-close:hover{color:var(--err)}
#repl-out{flex:1;overflow-y:auto;padding:12px 18px;font:13.5px/1.55 var(--mono);white-space:pre-wrap;word-break:break-word;cursor:text;scrollbar-width:thin}
.rline .ps{color:var(--accent);font-weight:700}
.rerr{color:#ff7b72}
#repl-inrow{display:flex;align-items:center;gap:10px;padding:10px 18px;border-top:1px solid var(--border);background:var(--bg3)}
#repl-ps{font:700 14px var(--mono);color:var(--accent);width:36px;flex:0 0 36px}
#repl-input{flex:1;background:transparent;border:0;outline:0;color:var(--text);font:14px var(--mono)}
#repl-fab{position:fixed;right:22px;bottom:22px;z-index:55;width:54px;height:54px;border-radius:50%;background:#238636;border:2px solid #2ea043;color:#fff;font:700 15px var(--mono);cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.45)}
#repl-fab:hover{background:#2ea043}
body.repl-open #repl-fab{display:none}
.open-repl-btn{background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:8px 16px;font:600 14px var(--sans);cursor:pointer}
.open-repl-btn:hover{background:color-mix(in srgb,var(--accent) 20%,transparent)}
/* ── toast / overlay ─────────────────────── */
#toast{position:fixed;bottom:92px;left:50%;transform:translate(-50%,16px);opacity:0;transition:.25s;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:10px 18px;border-radius:8px;z-index:70;pointer-events:none;font-size:14px;max-width:80vw}
#toast.show{opacity:1;transform:translate(-50%,0)}
#overlay{position:fixed;inset:0;background:rgba(1,4,9,.86);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:100;display:flex;align-items:center;justify-content:center;padding:20px}
#overlay .card{background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:34px 40px;max-width:480px;text-align:center}
#overlay h3{margin:0 0 8px}
#ov-status{color:var(--muted);font-size:14px;min-height:24px}
.spinner{width:42px;height:42px;border:4px solid var(--border);border-top-color:var(--accent);border-radius:50%;margin:0 auto 18px;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
#ov-btns{margin-top:18px;display:none;gap:10px;justify-content:center}
#ov-btns button{background:#238636;border:1px solid #2ea043;color:#fff;border-radius:6px;padding:8px 18px;font:600 14px var(--sans);cursor:pointer}
#ov-dismiss{display:inline-block;margin-top:12px;color:var(--muted);font-size:13px;cursor:pointer;text-decoration:underline}
hr.sep{border:0;border-top:1px solid var(--border);margin:26px 0}
footer.pagefoot{color:var(--muted);font-size:13px;text-align:center;margin-top:60px}
/* ── older-browser fallbacks (no color-mix support) ──────────────
Modern Firefox/Chrome/Edge/Safari use the color-mix() rules above.
This block only applies where color-mix() is unknown, so every
callout/quiz/badge still shows a readable background. */
@supports not (background:color-mix(in srgb,red 50%,blue)){
h2 .no{background:rgba(88,166,255,.12)}
.side-link.active{background:rgba(88,166,255,.10)}
.ed-out{background:var(--code-bg)}
.callout{background:var(--bg2)}
.callout.tip,.callout.warn,.callout.ana,.callout.crash{background:var(--bg2)}
.exercise{background:var(--bg2)}
.q label.chosen,.q.checked label.correct,.q.checked label.wrong{background:var(--bg3)}
.complete-btn.done{background:transparent}
.open-repl-btn{background:var(--bg2)}
}
</style>
</head>
<body>
<noscript><div>This course needs JavaScript to run its interactive code boxes.</div></noscript>
<div id="overlay">
<div class="card">
<div class="spinner" id="ov-spin"></div>
<h3>🐍 Waking up Python…</h3>
<div id="ov-status">Downloading the Python engine (~10 MB, first load only)…</div>
<div id="ov-btns"><button id="ov-retry">Try again</button></div>
<span id="ov-dismiss">keep reading the course anyway (code boxes stay read-only)</span>
</div>
</div>
<header id="topbar">
<button id="btn-menu" class="tb-btn" aria-label="Toggle contents">☰</button>
<div id="brand">🐍 <b>David's Python Intermediate</b><span class="tag">PyI · intermediate course · runs 100% in your browser</span></div>
<div id="progress-wrap"><div id="progress-bar"><div id="progress-fill"></div></div><span id="progress-text"></span></div>
<button id="btn-repl" class="tb-btn" title="Open the Python console (REPL)">>_ Console</button>
<button id="btn-reset" class="tb-btn" title="Clear all variables (fresh Python state)">↻ Reset</button>
<button id="btn-theme" class="tb-btn" title="Toggle dark / light">🌙</button>
</header>
<div id="layout">
<nav id="sidebar"></nav>
<main>
<!-- ══════════════ CHAPTER 0 ══════════════ -->
<section class="lesson" id="s0-bridge" data-chapter="0 · Bridge In" data-title="0.1 From scripts to programs">
<h2><span class="no">0.1</span>From scripts to programs <small>🟢 Browser-safe</small></h2>
<p>You finished Foundations able to write ~100-line programs: variables, loops, lists and dicts, functions, simple classes, files with <code>try/except</code>. This course takes you from there to <b>200–400-line tested, CLI-driven programs</b> built from the standard library plus <code>pytest</code>. Same language — bigger workshop.</p>
<table>
<tr><th>Foundations (you can)</th><th>Intermediate (you will)</th></tr>
<tr><td>One-file scripts, ~100 lines</td><td>Multi-file programs, 200–400 lines</td></tr>
<tr><td>Loops that build lists</td><td>Comprehensions, generators, lazy pipelines</td></tr>
<tr><td>Classes with <code>__init__</code></td><td>Dataclasses, dunders, properties, custom exceptions</td></tr>
<tr><td><code>print()</code> debugging</td><td><code>logging</code>, tracebacks, <code>pytest</code>, <code>pdb</code></td></tr>
<tr><td>Run by pressing a button</td><td>Run from a terminal: <code>venv</code>, <code>argparse</code>, real files</td></tr>
</table>
<div class="callout why"><span class="t">💡 Why structure is the whole game</span>
A 100-line script fits in your head all at once. A 400-line program doesn't — nobody's does. Modules, functions with one job, tests that pin behavior: these aren't ceremony, they're how you keep building when the program outgrows your working memory. Every technique in this course pays off the week your code stops fitting on one screen.</div>
<h3>The loop stays — plus one new layer</h3>
<p>Keep the Foundations habit verbatim: <b>Read → Predict → Run → Tinker → Break → Fix → Build</b>. Intermediate adds a layer between Predict and Run: <b>Trace</b>. Before running, ask <em>"what is <code>x</code> at line N?"</em> and work it out on paper. Solutions in this course show trace tables, not just answers.</p>
<script type="text/python" data-title="A taste of Module 1 — predict, trace, then run">
from collections import Counter
words = ["apple", "berry", "apple", "cherry", "berry", "apple"]
counts = Counter(words) # TRACE: what is counts["berry"] on line 8?
print(counts)
print(counts.most_common(2)) # the two most common words
total = sum(counts.values())
print("total words:", total) # in Foundations you'd write the loop by hand
</script>
<div class="callout tip"><span class="t">✅ Predict before you Run — every box</span>
Type a guess as a comment (<code># I think this prints ...</code>) before pressing Run. Wrong predictions are the fastest lessons in programming; this course is engineered to provoke them.</div>
<div class="callout warn"><span class="t">⚠️ A true infinite loop freezes the tab</span>
<code>while True:</code> with no <code>break</code> locks the page — that's the price of real Python in a tab. If it happens, reload: your code is auto-saved. Module 3 teaches lazy (possibly infinite) generators; every one of those boxes is guarded with <code>islice</code> or a counter.</div>
<h3>What this course is not</h3>
<p>Scope guard, stated once so no lesson smuggles these in: no async mastery, no metaclasses or descriptors, no Django/Flask production, no pandas/numpy track, no ML, no PyPI publishing pipeline (local <code>pip install -e .</code> at most). Suggestions in that direction belong in extensions.</p>
<h3>Where you're headed</h3>
<table>
<tr><th>Module</th><th>Question it answers</th></tr>
<tr><td>1 · Pythonic Data</td><td>How do I stop writing loops for everything?</td></tr>
<tr><td>2 · Functions as Objects</td><td>What can I do once functions are values?</td></tr>
<tr><td>3 · Lazy Python</td><td>How do I stream data bigger than memory?</td></tr>
<tr><td>4 · OOP That Pays</td><td>Which object tricks earn their complexity?</td></tr>
<tr><td>5 · Files, Data & Storage</td><td>How does data survive the program ending?</td></tr>
<tr><td>6 · Tooling & Quality</td><td>How do professionals run Python?</td></tr>
<tr><td>7 · Power Tools</td><td>How do decorators and context managers really work?</td></tr>
<tr><td>8 · Capstones</td><td>Can I ship three portfolio programs?</td></tr>
</table>
</section>
<section class="lesson" id="s0-diag" data-chapter="0 · Bridge In" data-title="0.2 Entry diagnostic — are you ready?">
<h2><span class="no">0.2</span>Entry diagnostic — are you ready? <small>🟢 Browser-safe</small></h2>
<p>Ten small tasks below, covering Foundations Modules 2–6. Rules: <b>no hints, no peeking</b> — do each from memory, then check against the solution. Count a task as passed only if your unaided version works.</p>
<ul>
<li><b>7+/10:</b> continue to Module 1.</li>
<li><b>5–6/10:</b> skim the linked Foundations modules first (table below), then continue.</li>
<li><b>Below 5:</b> redo Foundations Modules 2–6 properly, then come back. Module 1 assumes all of this is fluent.</li>
</ul>
<div class="exercise">
<h4>Exercise 0.1 — Ten tasks from memory</h4>
<p>Fix or finish each <code>TODO</code> so the box prints the expected values in the comments. Every task runs as-is (wrongly, or with a crash) — a crash just means "not solved yet".</p>
<script type="text/python" data-title="Diagnostic — ten TODOs">
score = 85
# TODO 1: print "pass" if score is 60 or more, else print "fail" → pass
print("TODO 1")
# TODO 2: add 1..10 with a for loop and print the total → 55
print("TODO 2")
colors = ["red", "green", "blue"]
# TODO 3: print the first and last color → red blue
print("TODO 3")
counts = {"apple": 3, "berry": 2}
# TODO 4: add 1 cherry with .get so missing keys don't crash, then print counts
print(counts)
# TODO 5: give name a default of "friend" so both calls below work
def greet(name):
print("Hi, " + name + "!")
# TODO 6: turn the JSON string into a dict and print its "a" value → 1
import json
data = '{"a": 1}'
d = {}
print(d["a"])
class Basket:
def __init__(self):
self.items = []
def add(item): # TODO 7: one word missing in this line — add it
self.items.append(item)
# TODO 8: make a Basket, add "apple", print b.items → ['apple']
print("TODO 8")
raw = "abc"
# TODO 9: wrap the conversion so this prints "bad number" instead of crashing
n = int(raw)
print("got:", n)
# TODO 10: write "hello file" to note.txt, read it back, print it
print("TODO 10")
</script>
<details class="hint"><summary>💡 Hints</summary><div>
<ul>
<li>1: <code>if score >= 60:</code> … <code>else:</code> … (prose uses <code>>=</code>; in code just type it).</li>
<li>2: <code>range(1, 11)</code> stops <em>before</em> 11.</li>
<li>3: <code>colors[0]</code> and <code>colors[-1]</code>.</li>
<li>4: <code>counts["cherry"] = counts.get("cherry", 0) + 1</code>.</li>
<li>5: <code>def greet(name="friend"):</code> — then call <code>greet()</code> and <code>greet("Ada")</code>.</li>
<li>6: <code>d = json.loads(data)</code>.</li>
<li>7: methods need <code>self</code> as the first parameter.</li>
<li>8: <code>b = Basket()</code>, then <code>b.add("apple")</code>.</li>
<li>9: <code>try:</code> the conversion, <code>except ValueError:</code> print the fallback.</li>
<li>10: <code>with open("note.txt", "w")</code> … <code>write(...)</code>, then again with <code>"r"</code> and <code>read()</code>.</li>
</ul>
</div></details>
<details class="solution"><summary>Show solution</summary><div>
<script type="text/python" data-title="Solution — diagnostic">
score = 85
if score >= 60:
print("pass")
else:
print("fail")
total = 0
for i in range(1, 11):
total = total + i
print(total)
colors = ["red", "green", "blue"]
print(colors[0], colors[-1])
counts = {"apple": 3, "berry": 2}
counts["cherry"] = counts.get("cherry", 0) + 1
print(counts)
def greet(name="friend"):
print("Hi, " + name + "!")
greet()
greet("Ada")
import json
data = '{"a": 1}'
d = json.loads(data)
print(d["a"])
class Basket:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
b = Basket()
b.add("apple")
print(b.items)
raw = "abc"
try:
n = int(raw)
print("got:", n)
except ValueError:
print("bad number")
with open("note.txt", "w") as f:
f.write("hello file")
with open("note.txt") as f:
print(f.read())
</script>
</div></details>
</div>
<h3>If you scored under 7 — revisit map</h3>
<table>
<tr><th>Tasks</th><th>Tests</th><th>Revisit in Foundations</th></tr>
<tr><td>1–2</td><td>decisions, loops</td><td>Module 2 · Control Flow</td></tr>
<tr><td>3–4</td><td>lists, dicts</td><td>Module 3 · Data Structures</td></tr>
<tr><td>5–6</td><td>functions, modules</td><td>Module 4 · Functions & Modules</td></tr>
<tr><td>7–8</td><td>classes, methods</td><td>Module 5 · Objects & Classes</td></tr>
<tr><td>9–10</td><td>errors, files</td><td>Module 6 · Files & Errors</td></tr>
</table>
</section>
<section class="lesson" id="s0-setup" data-chapter="0 · Bridge In" data-title="0.3 Your two machines — browser and terminal">
<h2><span class="no">0.3</span>Your two machines — browser and terminal <small>🔴 Terminal-required</small></h2>
<p>This course runs on <b>two machines</b>: the browser tab you're in now, and the terminal on your own computer. Rule of thumb for the whole course: <b>browser for concepts, terminal (or the desktop edition) for real files, CLIs, venv, and pytest.</b> Every lesson wears one badge so you always know where you are:</p>
<table>
<tr><th>Badge</th><th>Meaning</th><th>You…</th></tr>
<tr><td>🟢 Browser-safe</td><td>Runs identically in this tab (Pyodide)</td><td>press Run here</td></tr>
<tr><td>🟡 Browser-adapted</td><td>Same concept, shimmed I/O (in-memory files)</td><td>run here, read the banner, redo on your PC with real files</td></tr>
<tr><td>🔴 Terminal-required</td><td>Needs a real CLI: venv, multi-file, pytest</td><td>do it on your PC — the browser box is read-only</td></tr>
</table>
<div class="callout ana"><span class="t">🍪 Analogy — the library</span>
Your computer's file system is a <b>library</b>: shelves of files, a catalog, opening hours. The terminal is walking up to the <b>librarian's desk</b> and asking directly. This browser tab is a <b>borrowed desk</b> — same books, same reading skills, but you can't rearrange the shelves and everything is reshelved when you leave. This lesson is your first visit to the desk.</div>
<h3>Step 1 — check your Python</h3>
<p>Open a terminal (<b>Terminal</b> on macOS/Linux/ChromeOS, <b>PowerShell</b> on Windows) and run:</p>
<script type="text/x-python" data-static data-title="Terminal — check your version">
$ python3 --version
Python 3.12.3
</script>
<div class="callout warn"><span class="t">⚠️ python vs python3</span>
On macOS/Linux (and some Windows setups) bare <code>python</code> means Python 2 or nothing at all. Always type <code>python3</code> — and if even that fails, install Python 3.9+ from python.org, then come back. The desktop edition of this course must be launched the same way: <code>python3 py-intermediate.py</code>.</div>
<h3>Step 2 — one project, one venv</h3>
<p>A <b>venv</b> is a private Python playground per project: its own packages, no interference between projects. Make one where you'll keep course work:</p>
<script type="text/x-python" data-static data-title="Terminal — create and enter a venv">
$ mkdir pywork && cd pywork
$ python3 -m venv .venv
$ source .venv/bin/activate # Windows (PowerShell): .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install pytest
Collecting pytest
Successfully installed pytest-8.3.4
(.venv) $ python -m pytest --version
pytest 8.3.4
</script>
<table>
<tr><th>OS</th><th>Create</th><th>Activate</th></tr>
<tr><td>macOS / Linux / ChromeOS</td><td><code>python3 -m venv .venv</code></td><td><code>source .venv/bin/activate</code></td></tr>
<tr><td>Windows (PowerShell)</td><td><code>py -m venv .venv</code></td><td><code>.venv\Scripts\Activate.ps1</code></td></tr>
</table>
<div class="callout tip"><span class="t">🖥️ Open in terminal — do this now</span>
<ol>
<li><code>mkdir pywork && cd pywork</code> — one folder for all course work.</li>
<li><code>python3 -m venv .venv</code> — create the playground (once per folder).</li>
<li>Activate it (table above) — your prompt gains a <code>(.venv)</code> prefix.</li>
<li><code>python -m pip install pytest</code> — expected output ends with <code>Successfully installed pytest-...</code>.</li>
</ol>
Stuck? Re-read the error <em>last line first</em> (next lesson) — or ask with the full transcript pasted.</div>
<div class="callout warn"><span class="t">⚠️ Never sudo pip, never skip the venv</span>
<code>sudo pip install ...</code> (or bare <code>pip install</code> outside a venv) sprays packages into your system Python, where the OS depends on exact versions. Inside an active venv, plain <code>pip</code> is safe — the <code>(.venv)</code> prefix is your proof.</div>
<div class="callout tip"><span class="t">✅ The python -m habit</span>
<code>python -m pip ...</code> and <code>python -m pytest</code> always use <em>that</em> python's packages — even if you forgot to activate. If a command ever "can't find" an installed package, re-run it as <code>python -m ...</code> first.</div>
<h3>Step 3 — how a grown-up program is laid out</h3>
<p>From Module 6 on, starters look like this — a folder, not a file. The browser shows them flattened; your PC uses the real tree:</p>
<script type="text/x-python" data-static data-title="A real project tree (in pywork/)">
mytool/
.venv/ # created by python -m venv — never touch by hand
main.py # entry point: parses argv, calls helpers, prints
helpers.py # pure functions (easy to test!)
tests/
test_helpers.py # pytest discovers test_*.py automatically
</script>
<script type="text/x-python" data-static data-title="main.py — the entry-point idiom you'll use everywhere">
def main():
print("mytool does something useful")
if __name__ == "__main__":
main()
</script>
<p>Why the odd <code>if</code>? It means "run this only when the file is executed directly, not when imported for tests". Module 6 makes it second nature.</p>
<p>One more machine worth knowing: the <b>desktop edition</b> (<code>python3 py-intermediate.py</code>) sits between the two — same lessons, but code runs in a real subprocess with real files that persist in <code>~/.pyintermediate/workspace/</code>. Browser for concepts, desktop/terminal for reality.</p>
</section>
<section class="lesson" id="s0-trace" data-chapter="0 · Bridge In" data-title="0.4 Tracebacks — read the last line first">
<h2><span class="no">0.4</span>Tracebacks — read the last line first <small>🟢 Browser-safe</small></h2>
<p>You met tracebacks in Foundations; here they become your primary debugging tool. Anatomy of one — schematic, with notes where a real one names files:</p>
<script type="text/x-python" data-static data-title="Traceback anatomy (schematic)">
Traceback (most recent call last): ← the story starts here, ends at the crash
File "cart.py", line 9, in module ← WHERE, outermost frame (your file, line 9)
File "cart.py", line 5, in total ← WHERE, inner frame (your function, line 5)
TypeError: unsupported operand ... ← WHAT: read this line FIRST, always
</script>
<p>Read <b>bottom-up</b>: the last line names the error type and cause; the frames above are the call chain that got there, outermost first. Then fix, rerun, repeat.</p>
<script type="text/python" data-title="Trace it — what is running at line 11?">
# PREDICT first, then Run: on the LAST loop round, what is `running` at line 11?
# Trace table on paper — one row per round: p | running after line 11.
def add(a, b):
return a + b
prices = [4, 7, 2]
running = 0
for p in prices:
running = add(running, p)
print("after", p, "running now", running)
print("total:", running)
</script>
<div class="callout crash"><span class="t">🐞 Crash lab 1 — the typo (NameError)</span>
Run this, then read the <b>last line first</b>: <code>NameError</code> names the unknown variable, and modern Python even suggests the fix.
<script type="text/python" data-title="🐞 deliberate crash — NameError">
# DELIBERATE CRASH — Run it, read the LAST line, then open the fix.
user_name = "Ada"
print("Hello,", username) # NameError: which name is wrong?
</script>
<details class="solution"><summary>Show fix</summary><div>
<p>Last line: <code>NameError: name 'username' is not defined. Did you mean: 'user_name'?</code> — the variable is <code>user_name</code> (with underscore), the printout says <code>username</code>. Fix one character class:</p>
<script type="text/python" data-title="Fix — NameError">
user_name = "Ada"
print("Hello,", user_name)
</script>
</div></details>
</div>
<div class="callout crash"><span class="t">🐞 Crash lab 2 — the crash travels (ValueError through frames)</span>
This one works once, then crashes. The last line says <code>ValueError</code> — but the bug (bad input) is two frames up. Read bottom-up to find <em>your</em> line.
<script type="text/python" data-title="🐞 deliberate crash — ValueError through frames">
# DELIBERATE CRASH — the first call works. Read the frames to find whose fault "hot" is.
def to_celsius(f_str):
return (int(f_str) - 32) * 5 / 9
def report(raw):
c = to_celsius(raw)
return "It is " + str(round(c, 1)) + " C"
print(report("77")) # fine: 25.0 C
print(report("hot")) # ValueError — which of YOUR lines passed "hot" in?
</script>
<details class="solution"><summary>Show fix</summary><div>
<p>Frames point at <code>int(f_str)</code> (library-adjacent) ← <code>to_celsius(raw)</code> ← <code>report("hot")</code>: the bad value entered at the last line. Guard the boundary where outside data comes in:</p>
<script type="text/python" data-title="Fix — guard the boundary">
def to_celsius(f_str):
return (int(f_str) - 32) * 5 / 9
def report(raw):
try:
c = to_celsius(raw)
except ValueError:
return "can't convert " + raw
return "It is " + str(round(c, 1)) + " C"
print(report("77"))
print(report("hot"))
</script>
<p>Module 4 upgrades this to custom exceptions with <code>raise from</code>; Module 6 makes the boundary a CLI that never crashes ugly.</p>
</div></details>
</div>
<div class="callout tip"><span class="t">✅ When the traceback points inside a library, your bug is the last frame in YOUR file</span>
<code>int("hot")</code> "fails" inside <code>int</code> — but <code>int</code> is fine; <em>you</em> handed it <code>"hot"</code>. Scroll up past library frames to the deepest line of your own code. That's the fix site, almost every time.</div>
<div class="exercise">
<h4>Exercise 0.2 — Fix it from the traceback</h4>
<p>The box below crashes. Its traceback is printed underneath (as it appears when run). Find the fix site, fix it, rerun clean. Expected final output: <code>average of passing: 75.0</code>.</p>
<script type="text/python" data-title="Exercise 0.2 — broken average">
# TODO: fix the marked line so this prints: average of passing: 75.0
def average_of(scores, cutoff):
passing = []
for s in scores:
if s >= cutoff:
passing = passing + s # 💥 TypeError here — a list plus a what?
return sum(passing) / len(passing)
print(average_of([80, 45, 90, 55, 30], 50))
</script>
<script type="text/x-python" data-static data-title="The traceback it produces (read last line first)">
Traceback (most recent call last):
File "average.py", line 9, in module
File "average.py", line 6, in average_of
TypeError: can only concatenate list (not "int") to list
</script>
<details class="hint"><summary>💡 Hints</summary><div>
<ul>
<li>Last line first: <code>list + int</code> is illegal — line 5 tries <code>passing + s</code>, where <code>s</code> is one number.</li>
<li>The list method that adds <em>one item</em> is <code>append</code>.</li>
<li>After the fix, check the math by hand: passing scores are 80, 90, 55 — average 75.0.</li>
</ul>
</div></details>
<details class="solution"><summary>Show solution</summary><div>
<script type="text/python" data-title="Solution 0.2">
def average_of(scores, cutoff):
passing = []
for s in scores:
if s >= cutoff:
passing.append(s)
return sum(passing) / len(passing)
print(average_of([80, 45, 90, 55, 30], 50))
</script>
</div></details>
<div class="callout tip"><span class="t">🚀 Extensions (optional)</span>
<b>+robustness:</b> what if no score passes (empty <code>passing</code> → <code>ZeroDivisionError</code>)? Return <code>0.0</code> or a message instead. <b>+feature:</b> also return the count of passing scores. <b>+ship:</b> Module 6 turns this into a CLI flag (<code>--cutoff 50 scores.csv</code>) — keep the function pure so the CLI stays thin.</div>
</div>
<p>Module 0 done. You know what "intermediate" means, where you stand, where code runs, and how to read a crash. Module 1 starts the real upgrade: replacing loops with readable idioms.</p>
</section>
<!-- ══════════════ CHAPTER 1 ══════════════ -->
<section class="lesson" id="s1-comp" data-chapter="1 · Pythonic Data" data-title="1.1 Comprehensions — loops in one line">
<h2><span class="no">1.1</span>Comprehensions — loops in one line <small>🟢 Browser-safe</small></h2>
<p>Module 1 replaces hand-written loops with readable idioms. First and most-used: the <b>comprehension</b> — a loop that builds a list, set, or dict in a single expression. Trace this one before running:</p>
<script type="text/python" data-title="Loop first, then the one-liner">
# PREDICT + TRACE: what does `squares` hold after line 6? Then Run.
nums = [1, 2, 3, 4, 5]
squares = []
for n in nums:
squares.append(n * n) # line 6
print(squares)
# Same job, one line — no starter list, no append:
print([n * n for n in nums]) # line 10: [expression for item in source]
</script>
<p>The shape is always <code>[expression for item in source]</code> — read it as "give me <em>expression</em> for every <em>item</em>". Same pattern builds the other containers, with an optional filter:</p>
<script type="text/python" data-title="Set, dict, and filtered comprehensions">
names = ["Ada", "grace", "ALAN", "katherine"]
clean = [n.lower() for n in names if len(n) > 3]
print(clean) # condition filters BEFORE the expression runs
lengths = {n: len(n) for n in names}
print(lengths) # {key: value for ...} builds a dict
firsts = {n[0].lower() for n in names}
print(firsts) # {...} with one expression builds a SET
</script>
<h3>Nested — and when NOT to</h3>
<p>Two loops nest left-to-right (same order as writing them out). Use it for genuinely flat transformations — and reach for a plain loop the moment a reader would squint:</p>
<script type="text/python" data-title="Nested comps — use sparingly">
pairs = [(x, y) for x in [1, 2, 3] for y in "ab"]
print(pairs) # read left to right: x outer, y inner
# Clearer as a loop — a double comp here would be showing off:
matrix = [[1, 2], [3, 4]]
flat = []
for row in matrix:
for cell in row:
flat.append(cell)
print(flat)
</script>
<div class="callout why"><span class="t">💡 Why comps win</span>
Data work is 90% "shape this pile into that pile". Comprehensions say the shape in one glanceable line — no starter list three lines above the loop, no <code>append</code> to hunt for. When the transformation doesn't fit one line, that's the comp telling you to write the loop (or a function). Both are respectable; guessing wrong is a style issue, not a bug.</div>
<div class="callout crash"><span class="t">🐞 Crash lab — never mutate while iterating</span>
Deleting inside a loop shifts everything left under your feet — items get <b>skipped silently</b>. Predict the output, run, count the survivors.
<script type="text/python" data-title="🐞 deliberate crash-ish — skipped items">
# NOT a crash — worse: silently wrong. Which numbers survive?
nums = [1, 2, 2, 3, 4, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n) # shifts the list mid-loop!
print(nums) # expected [1, 3, 5] — is it?
</script>
<details class="solution"><summary>Show fix</summary><div>
<p>Output is <code>[1, 2, 3, 4, 5]</code> — a <code>2</code> and a <code>4</code> survived. After removing the first <code>2</code>, the second <code>2</code> slid into the checked slot and was never examined. Build a new list instead of mutating:</p>
<script type="text/python" data-title="Fix — build, don't mutate">
nums = [1, 2, 2, 3, 4, 4, 5]
odds = [n for n in nums if n % 2 != 0]
print(odds) # [1, 3, 5] — nothing skipped, original untouched
</script>
</div></details>
</div>
</section>
<section class="lesson" id="s1-zip" data-chapter="1 · Pythonic Data" data-title="1.2 enumerate, zip, sorted — loops that count and pair">
<h2><span class="no">1.2</span><code>enumerate</code>, <code>zip</code>, <code>sorted</code> — loops that count and pair <small>🟢 Browser-safe</small></h2>
<p>Need an index? Don't count by hand — <code>enumerate</code> counts for you. Need two lists side by side? <code>zip</code> pairs them. Need order? <code>sorted</code> with a <code>key</code> beats every hand-rolled sort:</p>
<script type="text/python" data-title="enumerate and zip">
names = ["ada", "grace", "katherine"]
for i, name in enumerate(names, start=1):
print(i, name) # start=1 for human numbering
scores = [98, 100, 87]
for name, score in zip(names, scores):
print(name, "scored", score)
</script>
<script type="text/python" data-title="sorted with key">
words = ["banana", "Fig", "apple", "cherry"]
print(sorted(words)) # 'Fig' first — capitals sort before lowercase!
print(sorted(words, key=str.lower)) # compare lowercase, keep originals
print(sorted(words, key=len)) # shortest first
users = [("ada", 36), ("grace", 85), ("katherine", 67)]
def age(user):
return user[1]
print(sorted(users, key=age, reverse=True)) # oldest first, originals untouched
</script>
<div class="callout tip"><span class="t">✅ sorted returns new; .sort rearranges</span>
<code>sorted(data)</code> hands you a fresh list, <code>data</code> unchanged. <code>data.sort()</code> shuffles <code>data</code> in place and returns <code>None</code>. Default to <code>sorted</code> unless you deliberately want mutation — one fewer surprise.</div>
<div class="callout warn"><span class="t">⚠️ zip stops at the SHORTER list — silently</span>
<script type="text/python" data-title="zip truncation demo">
names = ["ada", "grace", "katherine"]
scores = [98, 100] # one score missing — no error, katherine just vanishes
print(list(zip(names, scores)))
</script>
Guard it: <code>assert len(names) == len(scores)</code> before zipping, or (Python 3.10+) <code>zip(names, scores, strict=True)</code>, which raises instead of dropping. Silent data loss is the worst kind of bug — make length mismatches loud.</div>
</section>
<section class="lesson" id="s1-unpack" data-chapter="1 · Pythonic Data" data-title="1.3 Unpacking — *args and **kwargs">
<h2><span class="no">1.3</span>Unpacking — <code>*args</code> and <code>**kwargs</code> <small>🟢 Browser-safe</small></h2>
<p>Assignment can split a collection apart in one move. The star <code>*</code> soaks up "everything else" — in assignment, in calls, and in function signatures:</p>
<script type="text/python" data-title="Splitting with *">
a, b = "coffee", "tea"
a, b = b, a # the Foundations swap, in one line
print(a, b)
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last) # middle is always a LIST: [2, 3, 4]
rows = [("ada", 36, "berlin"), ("grace", 85, "london")]
for name, age, city in rows: # unpack right in the loop header
print(name, "is", age, "in", city)
</script>
<script type="text/python" data-title="*args and **kwargs">
# *nums gathers extra POSITIONAL args into a tuple,
# **labels gathers extra KEYWORD args into a dict.
def report(title, *nums, **labels):
print(title, sum(nums), labels)
report("scores", 10, 20, 30, unit="pts", curve=True)
args = [4, 5, 6]
print(*args) # * in a CALL unpacks: print(4, 5, 6)
print(*args, sep=" | ") # unpacking + sep is a tiny report writer
</script>
<div class="callout tip"><span class="t">✅ Read signatures with stars in them</span>
<code>def f(a, b, *rest)</code>: two required, then any extras as a tuple. <code>def f(**kw)</code>: any keywords as a dict. <code>def f(*, flag)</code>: everything after a bare <code>*</code> MUST be passed by keyword. You'll read these in every library; Module 6's CLIs write them.</div>
</section>
<section class="lesson" id="s1-collections" data-chapter="1 · Pythonic Data" data-title="1.4 Counter, defaultdict, namedtuple">
<h2><span class="no">1.4</span><code>Counter</code>, <code>defaultdict</code>, <code>namedtuple</code> <small>🟢 Browser-safe</small></h2>
<p>Three <code>collections</code> tools that delete whole categories of loop. You tasted <code>Counter</code> in Module 0 — here's the trio doing real grouping and record-keeping:</p>
<script type="text/python" data-title="Counting and grouping without boilerplate">
from collections import Counter, defaultdict
votes = ["yes", "no", "yes", "yes", "blank", "no", "yes"]
tally = Counter(votes)
print(tally)
print(tally.most_common(2)) # winners first — leaderboards in one call
by_len = defaultdict(list) # missing key? auto-made empty list, no crash
for w in ["apple", "fig", "banana", "kiwi", "plum"]:
by_len[len(w)].append(w)
print(dict(by_len))
</script>
<script type="text/python" data-title="namedtuple — tiny readable records">
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p) # Point(x=3, y=4) — self-describing, unlike (3, 4)
print(p.x + p.y) # fields by NAME, still a tuple underneath
# p.x = 9 # uncomment: AttributeError — immutable, like all tuples
</script>
<div class="callout crash"><span class="t">🐞 Crash lab — the missing key (KeyError)</span>
Counting by hand crashes on the first new word — the lookup runs before the key exists. Read the last line, then fix it two ways.
<script type="text/python" data-title="🐞 deliberate crash — KeyError">
# DELIBERATE CRASH — which line meets a key that isn't there yet?
counts = {}
for w in ["a", "b", "a"]:
counts[w] = counts[w] + 1 # KeyError on "a" — the FIRST time, nothing to +1
print(counts)
</script>
<details class="solution"><summary>Show fixes</summary><div>
<p>Last line: <code>KeyError: 'a'</code>. Fix 1 — <code>.get</code> with a default. Fix 2 — stop hand-rolling and use the lesson's tools:</p>
<script type="text/python" data-title="Fixes — get, defaultdict, Counter">
counts = {}
for w in ["a", "b", "a"]:
counts[w] = counts.get(w, 0) + 1
print(counts) # fix 1: .get never crashes
from collections import Counter, defaultdict
print(Counter(["a", "b", "a"])) # fix 2a: the one-liner
d = defaultdict(int) # fix 2b: missing key auto-starts at 0
for w in ["a", "b", "a"]:
d[w] = d[w] + 1
print(dict(d))
</script>
</div></details>
</div>
<div class="callout warn"><span class="t">⚠️ Dict comprehensions silently eat duplicate keys</span>
<code>{w: len(w) for w in ["a", "bb", "a"]}</code> keeps only the LAST <code>"a"</code> — no error, data just gone. When keys might repeat, that's a <code>Counter</code> (count them) or a <code>defaultdict(list)</code> (keep them all), never a plain dict comp.</div>
<div class="callout tip"><span class="t">✅ namedtuple now, dataclass later</span>
<code>namedtuple</code> is the 30-second record: immutable, tuple-compatible, named fields. When you need defaults, validation, or methods, Module 4's <code>@dataclass</code> is the upgrade path — same idea, grown up.</div>
</section>
<section class="lesson" id="s1-idioms" data-chapter="1 · Pythonic Data" data-title="1.5 Idioms — join, truthiness, any/all">
<h2><span class="no">1.5</span>Idioms — <code>join</code>, truthiness, <code>any</code>/<code>all</code> <small>🟢 Browser-safe</small></h2>
<p>The small habits that mark intermediate code: build strings with <code>join</code>, lean on truthiness, ask <code>any</code>/<code>all</code> instead of flag variables:</p>
<script type="text/python" data-title="join beats plus-equals">
import time
words = ["word" + str(i) for i in range(20000)]
t = time.perf_counter()
s = ""
for w in words:
s = s + w # copies the whole string EVERY round
slow = time.perf_counter() - t
t = time.perf_counter()
s = "".join(words) # one pass, one allocation
fast = time.perf_counter() - t
print("plus-equals: %.3fs" % slow)
print("join: %.3fs" % fast) # typically 50–200x faster
</script>
<script type="text/python" data-title="Truthiness, or-default, any/all">
name = ""
display = name or "anonymous" # empty string is falsy → fallback wins
print(display)
readings = [0.5, 0.0, 0.7]
print(any(r == 0.0 for r in readings)) # any failures? (no flag variable!)
print(all(r > 0.0 for r in readings)) # all healthy?
if display in ("anonymous", "guest"):
print("limited mode") # `in` a tuple beats display == "a" or display == "g"
</script>
<div class="callout tip"><span class="t">✅ Falsy in one line</span>
<code>None</code>, <code>False</code>, <code>0</code>, <code>""</code>, <code>[]</code>, <code>{}</code> are all falsy — everything else is truthy. So <code>if items:</code> means "non-empty", and <code>value or default</code> means "default when missing-ish". Careful: <code>0 or 5</code> is <code>5</code> — if zero is a <em>valid</em> value, test <code>is None</code> explicitly.</div>
</section>
<section class="lesson" id="s1-check" data-chapter="1 · Pythonic Data" data-title="1.6 Checkpoint — quiz and exercises">
<h2><span class="no">1.6</span>Checkpoint — quiz and exercises <small>🟢 Browser-safe</small></h2>
<p>Five questions, then three builds. The quiz wants 4/5; the exercises want working code.</p>
<div class="quiz" data-quiz="qi1"></div>
<div class="exercise">
<h4>Exercise 1.1 — Loop to comp (paraphrase)</h4>
<p>Rewrite the loop as a single comprehension. Expected: <code>[0, 6, 12, 18]</code>.</p>
<script type="text/python" data-title="Exercise 1.1 — loop to comp">
nums = [0, 1, 2, 3, 4, 5, 6]
# TODO: one-line comp — triple the even numbers only
result = []
for n in nums:
if n % 2 == 0:
result.append(n * 3)
print(result)
</script>
<details class="hint"><summary>💡 Hints</summary><div>
<ul>
<li>Shape: <code>[expression for item in source if condition]</code> — three slots, in that order.</li>
<li>Expression triples, condition keeps evens.</li>
</ul>
</div></details>
<details class="solution"><summary>Show solution</summary><div>
<script type="text/python" data-title="Solution 1.1">
nums = [0, 1, 2, 3, 4, 5, 6]
result = [n * 3 for n in nums if n % 2 == 0]
print(result)
</script>
</div></details>
<p><b>🚀 Extensions (optional):</b> <b>+feature:</b> also collect the odds tripled into a second comp. <b>+robustness:</b> make it a function <code>triple_evens(nums)</code> that returns <code>[]</code> for an empty input. <b>+ship:</b> time both versions with <code>time.perf_counter</code> on 1M numbers.</p>
</div>
<div class="exercise">
<h4>Exercise 1.2 — The mystery surcharge (debug)</h4>
<p>This till reader crashes on real data. Run it, read the last line, fix the lookup so unknown fruit costs 0. Expected total: <code>8</code>.</p>
<script type="text/python" data-title="Exercise 1.2 — KeyError at the till">
# TODO: one-word-class fix — unknown fruit should cost 0, not crash
prices = {"apple": 2, "fig": 3}
basket = ["apple", "fig", "plum", "apple"]
total = sum(prices[f] for f in basket)
print("total:", total) # should print: total: 8
</script>
<details class="hint"><summary>💡 Hints</summary><div>
<ul>
<li>Last line first: <code>KeyError: 'plum'</code> — the comp meets a fruit with no price.</li>
<li>Lesson 1.4's fix: <code>.get</code> with a default of 0.</li>
</ul>
</div></details>
<details class="solution"><summary>Show solution</summary><div>
<script type="text/python" data-title="Solution 1.2">
prices = {"apple": 2, "fig": 3}
basket = ["apple", "fig", "plum", "apple"]
total = sum(prices.get(f, 0) for f in basket)
print("total:", total)
</script>
</div></details>
<p><b>🚀 Extensions (optional):</b> <b>+feature:</b> also print a <code>Counter</code> of the basket. <b>+robustness:</b> collect the unknown fruits into a list and print "unpriced: ..." instead of silently charging 0. <b>+ship:</b> read the basket from <code>input()</code> split on commas.</p>
</div>
<div class="exercise">
<h4>Exercise 1.3 — Top-words reporter (build)</h4>
<p>Finish the reporter: normalize the text, count, show the top <code>n</code>. ~25 lines when done.</p>
<script type="text/python" data-title="Exercise 1.3 — top-words skeleton">
from collections import Counter
TEXT = "Apple banana apple. Cherry! banana, apple? cherry apple."
# TODO: lowercase everything, strip the characters ".,!?" (see hint),
# split into words, Counter them, print the top 2 with most_common.
# Expected: apple 4, then banana 2 (order of the top 2).
print("TODO")
</script>
<details class="hint"><summary>💡 Hints</summary><div>
<ul>
<li><code>TEXT.lower()</code>, then loop the punctuation chars replacing each with <code>""</code> — or chain <code>.replace(".", "")</code> calls.</li>
<li><code>Counter(words).most_common(2)</code> gives <code>[("apple", 4), ("banana", 2)]</code> — unpack in a loop to print pretty lines.</li>
</ul>
</div></details>
<details class="solution"><summary>Show solution</summary><div>
<script type="text/python" data-title="Solution 1.3">
from collections import Counter
TEXT = "Apple banana apple. Cherry! banana, apple? cherry apple."
clean = TEXT.lower()
for mark in ".,!?":
clean = clean.replace(mark, "")
tally = Counter(clean.split())
for word, count in tally.most_common(2):
print(word, count)
</script>
</div></details>
<p><b>🚀 Extensions (optional):</b> <b>+feature:</b> take <code>n</code> from the user (guard the <code>int()</code> like Module 0). <b>+robustness:</b> skip words shorter than 3 letters. <b>+ship:</b> this exact core returns inside Capstone B — keep it pure (no <code>input</code> inside the counting).</p>
</div>
</section>
<!-- ══════════════ CHAPTER 2 ══════════════ -->
<section class="lesson" id="s2-firstclass" data-chapter="2 · Functions as Objects" data-title="2.1 First-class functions — plus lambda discipline">
<h2><span class="no">2.1</span>First-class functions — plus lambda discipline <small>🟢 Browser-safe</small></h2>
<p><b>First-class</b> means functions are ordinary values: store them in dicts, pass them as arguments, return them from functions. The moment you stop seeing <code>def</code> as special syntax and start seeing functions as things you can hand around, a shelf of techniques opens:</p>
<script type="text/python" data-title="Dispatch dict — no if/elif chain">
# Functions as values: pick behavior at RUNTIME by looking it up.
def add(a, b):
return a + b
def mul(a, b):
return a * b
ops = {"add": add, "mul": mul}
print(ops["add"](3, 4)) # 7
print(ops["mul"](3, 4)) # 12
chosen = ops["mul"] # a function sitting in a variable, like any value
print(chosen(5, 6)) # 30
</script>
<p>And the tiny anonymous sibling — <code>lambda</code> makes a one-expression function with no name. Discipline matters more than syntax here:</p>
<script type="text/python" data-title="lambda — one expression, then stop">
words = ["banana", "Fig", "apple"]
print(sorted(words, key=str.lower)) # best: pass an EXISTING named function
print(sorted(words, key=lambda w: w.lower())) # ok: one tiny expression
print(min(words, key=len)) # len IS the function — no lambda needed
# Anything with a statement, a condition worth naming, or a second use
# gets a def with a real name. Future-you greps for names, not lambdas.
</script>
<div class="callout why"><span class="t">💡 Why first-class pays rent</span>
Dispatch tables kill sprawling <code>if/elif</code> chains (commands, menu handlers, file-format readers). Passing <code>key=</code> functions customizes <code>sorted</code>/<code>min</code>/<code>max</code> without rewriting them. Returning functions builds factories (next lesson). Every decorator and callback in Modules 6–8 is just this idea, dressed up.</div>
<div class="callout tip"><span class="t">✅ The one-line lambda rule</span>
If the lambda fits on one line <em>and</em> you'd never reuse or test it, keep it. Longer, conditional (<code>lambda x: a if c else b</code>), or needed twice → <code>def</code>. Linters flag assigned lambdas (<code>f = lambda: ...</code>) — that's just a <code>def</code> with extra steps.</div>
</section>
<section class="lesson" id="s2-closure" data-chapter="2 · Functions as Objects" data-title="2.2 Closures — functions that remember">
<h2><span class="no">2.2</span>Closures — functions that remember <small>🟢 Browser-safe</small></h2>
<p>A <b>closure</b> is a function that remembers variables from where it was born — even after that scope finished. State without a class, without globals:</p>
<script type="text/python" data-title="A counter with no class, no globals">
def make_counter():
total = 0
def count(step=1):
nonlocal total # write access to the REMEMBERED variable (read needs nothing)
total = total + step
return total
return count # return the inner function — it carries `total` along
clicks = make_counter()
print(clicks(), clicks(), clicks(5)) # 1 2 7 — state lives in the closure
print(clicks.__name__) # 'count' — it IS the inner function
</script>
<div class="callout crash"><span class="t">🐞 Crash lab 1 — late binding: every button remembers the SAME variable</span>
The classic. Three lambdas close over <code>i</code> — the <em>variable</em>, not its value. By call time the loop is done. Predict, run, watch <code>[0, 1, 2]</code> not appear.
<script type="text/python" data-title="🐞 deliberate surprise — late binding">
# NOT a crash — silently shared. What does each func remember?
funcs = []
for i in range(3):
funcs.append(lambda: i) # captures VARIABLE i (currently heading to 2)
print([f() for f in funcs]) # expect [0, 1, 2] — is it?
</script>
<details class="solution"><summary>Show fix</summary><div>
<p>Output is <code>[2, 2, 2]</code> — all three read <code>i</code> <em>after</em> the loop left it at 2. Freeze each value with a default argument (defaults evaluate at <code>def</code> time):</p>
<script type="text/python" data-title="Fix — freeze with a default">
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default evaluated NOW, per round
print([f() for f in funcs]) # [0, 1, 2]
</script>
</div></details>
</div>