-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
1026 lines (639 loc) · 133 KB
/
index.html
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>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>
</title>
<meta property="og:type" content="website">
<meta property="og:title">
<meta property="og:url" content="http://mpkocher.github.io/index.html">
<meta property="og:site_name">
<meta property="og:locale" content="en_US">
<meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@https://twitter.com/ElevenPhonons">
<link rel="alternative" href="/atom.xml" title="" type="application/atom+xml">
<link rel="icon" href="/favicon.png">
<link rel="stylesheet" href="/perfect-scrollbar/css/perfect-scrollbar.min.css">
<link rel="stylesheet" href="/styles/main.css">
<!-- Google Analytics -->
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-141397134-1', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
<meta name="generator" content="Hexo 6.2.0"></head>
<body
class="monochrome"
>
<div class="mobile-header">
<button class="sidebar-toggle" type="button">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="title" href="/"></a>
</div>
<div class="main-container">
<div class="sidebar">
<div class="header">
<h1 class="title"><a href="/"></a></h1>
<div class="info">
<div class="content">
</div>
<div class="avatar">
<a href="/2017/09/04/About"><img src="https://avatars3.githubusercontent.com/u/868552?v=4&s=460"></a>
</div>
</div>
</div>
<div class="body">
<ul class="nav">
<li class="tag-list-container">
<a href="javascript:;">Tag</a>
<ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/FPT/" rel="tag">FPT</a><span class="tag-list-count">5</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/functional/" rel="tag">functional</a><span class="tag-list-count">5</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/lbl/" rel="tag">lbl</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/materials-project/" rel="tag">materials-project</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/mypy/" rel="tag">mypy</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/pacbio/" rel="tag">pacbio</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/python/" rel="tag">python</a><span class="tag-list-count">10</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/scala/" rel="tag">scala</a><span class="tag-list-count">2</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/scala-cli/" rel="tag">scala-cli</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/series/" rel="tag">series</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/zio/" rel="tag">zio</a><span class="tag-list-count">1</span></li></ul>
</li>
<li class="archive-list-container">
<a href="javascript:;">Archive</a>
<ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/archives/2022/">2022</a><span class="archive-list-count">1</span></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/">2019</a><span class="archive-list-count">9</span></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/">2017</a><span class="archive-list-count">3</span></li></ul>
</li>
</ul>
<ul class="nav">
<li>
<a href="/" title="Homepage" external="false">Homepage</a>
</li>
<li>
<a href="/archives" title="By Year" external="false">By Year</a>
</li>
<li>
<a href="/2019/03/01/Functional-Programming-Techniques-In-Python-Series/" title="Functional Programming Techniques in Python" external="false">Functional Programming Techniques in Python</a>
</li>
</ul>
<ul class="nav">
<li>
<a href="/2017/09/04/About/" title="About" external="false">About</a>
</li>
<li>
<a href="https://github.com/mpkocher/cv/blob/master/academic/cv.pdf" title="CV" target="_blank" rel="noopener">CV</a>
</li>
<li>
<a href="https://github.com/mpkocher" title="Github" target="_blank" rel="noopener">Github</a>
</li>
<li>
<a href="https://gist.github.com/mpkocher" title="Gists" target="_blank" rel="noopener">Gists</a>
</li>
<li>
<a href="https://www.linkedin.com/in/michael-kocher-b4bba5122/" title="LinkedIn" target="_blank" rel="noopener">LinkedIn</a>
</li>
<li>
<a href="https://twitter.com/ElevenPhonons" title="Twitter" target="_blank" rel="noopener">Twitter</a>
</li>
<li>
<a href="/2017/09/04/Now" title="Now" external="false">Now</a>
</li>
<li>
<a href="/atom.xml" title="RSS" external="false">RSS</a>
</li>
</ul>
</div>
</div>
<div class="main-content">
<div style="max-width: 1000px">
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2022/06/02/CLI-App-leveraging-ZIO-and-Decline-using-scala-cli/" >
Creating CLI tools leveraging ZIO and Decline using scala-cli
</a>
</h3>
<div class="article-info">
<a href="/2022/06/02/CLI-App-leveraging-ZIO-and-Decline-using-scala-cli/"><span class="article-date">
2022-06-02
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/scala/" rel="tag">scala</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/scala-cli/" rel="tag">scala-cli</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/zio/" rel="tag">zio</a></li></ul>
</span>
</div>
<div class="article-entry">
<p>Here’s a quick overview and demonstration of kicking the tires on ZIO 1.x + <a target="_blank" rel="noopener" href="https://github.com/bkirwi/decline">decline</a> (commandline argument parser) using <code>scala-cli</code> (version 0.1.6) to build commandline tools.</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://scala-cli.virtuslab.org/">https://scala-cli.virtuslab.org</a></li>
<li><a target="_blank" rel="noopener" href="https://zio.dev/">https://zio.dev</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/bkirwi/decline">https://github.com/bkirwi/decline</a></li>
<li><a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/e7b23d082a9ce9cba6e5ec3337658e76">Complete Gist</a></li>
</ul>
<h2 id="High-Level-Points"><a href="#High-Level-Points" class="headerlink" title="High Level Points"></a>High Level Points</h2><ul>
<li><code>scala-cli</code> is a new tool in active development that aims to replace the current <code>scala</code> tool. </li>
<li><code>scala-cli</code> enables running scripts, loading REPL, compiling, testing, packaging (amongst other features) for “simple” (flat) projects.</li>
<li><code>scala-cli</code> enables a very Python-ish workflow and integrates with your text editor well. A REPL driven approach can be used using <code>scala-cli repl File.scala</code>.</li>
<li><code>scala-cli package</code> enables <a target="_blank" rel="noopener" href="https://scala-cli.virtuslab.org/docs/cookbooks/scala-package">creating an executable</a> from your main class. This is very useful. No need to deal with Python’s conda, pipenv, poetry, etc… for setting up an env. Rsync/scp your packaged tool to another server and your good to go (provided the java versions are compatible).</li>
<li>Building a commandline tool using ZIO was a useful exercise to kick the tires on ZIO and understand the ZIO 1.x effect system and learn how to compose computation in ZIO.</li>
<li><code>decline</code> is a CLI parser library using <code>cats</code>. It has a very elegant mechanism of composing options/commands. </li>
<li>The default <code>decline</code> interface had some unexpected behaviors with how <code>--help</code> was handled and how errors were handled/mapped to exit codes.</li>
</ul>
<h2 id="Specifics"><a href="#Specifics" class="headerlink" title="Specifics"></a>Specifics</h2><p>Using <code>scala-cli setup-ide .</code> will generate the necessary files for the LSP server for your text editor.</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://scala-cli.virtuslab.org/docs/commands/setup-ide">https://scala-cli.virtuslab.org/docs/commands/setup-ide</a></li>
</ul>
<p>Using <code>directives</code> <code>//></code> in the top of your scala file, you can define the scala version, library versions, etc…</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://scala-cli.virtuslab.org/docs/reference/directives">https://scala-cli.virtuslab.org/docs/reference/directives</a></li>
</ul>
<p>For example, <code>Declined.scala</code></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//> using platform "jvm"</span><br><span class="line">//> using scala "2.13.8"</span><br><span class="line">//> using lib "dev.zio::zio:1.0.14"</span><br><span class="line">//> using lib "com.monovore::decline:2.2.0"</span><br><span class="line">//> using mainClass "DeclinedApp"</span><br></pre></td></tr></table></figure>
<p>Using <code>zio.App</code> we can define our <code>main</code> by overriding <code>run</code>.</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">object</span> <span class="title">DeclinedApp</span> <span class="keyword">extends</span> <span class="title">zio</span>.<span class="title">App</span> </span>{</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">def</span> <span class="title">run</span></span>(args: <span class="type">List</span>[<span class="type">String</span>]): <span class="type">ZIO</span>[<span class="type">ZEnv</span>, <span class="type">Nothing</span>, <span class="type">ExitCode</span>] = ???</span><br><span class="line">} </span><br></pre></td></tr></table></figure>
<p>Using <code>decline</code>, CLI options and arguments are defined using <code>Opts</code>.</p>
<p>For example:</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.monovore.decline._</span><br><span class="line"><span class="keyword">import</span> cats.implicits._</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> nameOpt = <span class="type">Opts</span>.option[<span class="type">String</span>](<span class="string">"user"</span>, help = <span class="string">"User name"</span>, <span class="string">"u"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> alphaOpt = <span class="type">Opts</span></span><br><span class="line"> .option[<span class="type">Double</span>](<span class="string">"alpha"</span>, help = <span class="string">"Alpha Filtering"</span>, <span class="string">"a"</span>)</span><br><span class="line"> .withDefault(<span class="number">1.23</span>)</span><br></pre></td></tr></table></figure>
<p>These <code>Opts</code> compose in interesting ways, specifically with <code>ZIO</code> effects.</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> versionOpt: <span class="type">Opts</span>[<span class="type">RIO</span>[<span class="type">Console</span>, <span class="type">Unit</span>]] = <span class="type">Opts</span></span><br><span class="line"> .flag(</span><br><span class="line"> <span class="string">"version"</span>,</span><br><span class="line"> <span class="string">"Show version and Exit"</span>,</span><br><span class="line"> <span class="string">"v"</span>,</span><br><span class="line"> visibility = <span class="type">Visibility</span>.<span class="type">Partial</span></span><br><span class="line"> )</span><br><span class="line"> .orFalse</span><br><span class="line"> .map(_ => putStrLn(<span class="type">VERSION</span>))</span><br></pre></td></tr></table></figure>
<p>You can compose options together using <code>mapN</code> to define an “action” or “command” that would need multiple commandline options/args.</p>
<p>For example to define a <code>mainOpt</code> that uses <code>name, alpha, force</code>:</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> nameOpt = <span class="type">Opts</span>.option[<span class="type">String</span>](<span class="string">"user"</span>, help = <span class="string">"User name"</span>, <span class="string">"u"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> alphaOpt = <span class="type">Opts</span></span><br><span class="line"> .option[<span class="type">Double</span>](<span class="string">"alpha"</span>, help = <span class="string">"Alpha Filtering"</span>, <span class="string">"a"</span>)</span><br><span class="line"> .withDefault(<span class="number">1.23</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add for testing failure modes</span></span><br><span class="line"><span class="keyword">val</span> forceOpt = <span class="type">Opts</span>.flag(<span class="string">"fail"</span>, help = <span class="string">"Manually trigger a failure"</span>).orFalse</span><br><span class="line"></span><br><span class="line"><span class="comment">// Now define our core</span></span><br><span class="line"><span class="keyword">val</span> mainOpt: <span class="type">Opts</span>[<span class="type">RIO</span>[<span class="type">Console</span>, <span class="type">Unit</span>]] =</span><br><span class="line"> (nameOpt, alphaOpt, forceOpt).mapN[<span class="type">RIO</span>[<span class="type">Console</span>, <span class="type">Unit</span>]] {</span><br><span class="line"> (name, alpha, force) =></span><br><span class="line"> <span class="keyword">if</span> (force)</span><br><span class="line"> <span class="type">ZIO</span>.fail(</span><br><span class="line"> <span class="keyword">new</span> <span class="type">Exception</span>(<span class="string">s"Manually FAIL triggered by <span class="subst">$name</span>! alpha=<span class="subst">$alpha</span>"</span>)</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">else</span> putStrLn(<span class="string">s"Hello <span class="subst">$name</span>. Running with alpha=<span class="subst">$alpha</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>In addition to composing using <code>mapN</code>, there’s <code>orElse</code> which enables composing “actions”. For example, enabling <code>--version</code> to run (if provided) <em>or</em> run the “main” application.</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> runOpt = versionOpt orElse mainOpt</span><br></pre></td></tr></table></figure>
<p>These composed <code>Opts</code> can be used in a <code>Command</code> that will handled <code>--help</code> and be central point where <code>Command.parse</code> can be called. </p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> command: <span class="type">Command</span>[<span class="type">RIO</span>[<span class="type">Console</span>, <span class="type">Unit</span>]] = <span class="type">Command</span>[<span class="type">RIO</span>[<span class="type">Console</span>, <span class="type">Unit</span>]](</span><br><span class="line"> name = <span class="string">"declined"</span>,</span><br><span class="line"> header = <span class="string">"Testing decline+zio"</span></span><br><span class="line"> )(versionOpt orElse mainOpt)</span><br></pre></td></tr></table></figure>
<p>Bridging the ouptut of <code>Command.parse</code> with ZIO requires a little glue and some special attention to deal with the error cases. <code>Command.parse</code> will return an <code>Either[Help, T]</code>. The left of <code>Either</code> being used as “help+errors” container is a bit of friction point because <code>--help</code> triggers the left of the <code>Either</code>. <code>Help.errors</code> will return a non-empty list of errors (if there are parse errors). </p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">effect</span></span>(arg: <span class="type">List</span>[<span class="type">String</span>]): <span class="type">ZIO</span>[<span class="type">Console</span>, <span class="type">Throwable</span>, <span class="type">Unit</span>] = {</span><br><span class="line"> command.parse(arg) <span class="keyword">match</span> {</span><br><span class="line"> <span class="keyword">case</span> <span class="type">Left</span>(help) => <span class="comment">// a bit odd that --help returns here</span></span><br><span class="line"> <span class="keyword">if</span> (help.errors.isEmpty) putStrLn(help.show)</span><br><span class="line"> <span class="keyword">else</span> <span class="type">IO</span>.fail(<span class="keyword">new</span> <span class="type">Exception</span>(<span class="string">s"<span class="subst">${help.errors}</span>"</span>))</span><br><span class="line"> <span class="keyword">case</span> <span class="type">Right</span>(value) => value.map(_ => ())</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>And finally, wire a call to <code>run</code> and make sure errors are written to stderr and a non-zero exit code is returned during failure cases. </p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">def</span> <span class="title">run</span></span>(args: <span class="type">List</span>[<span class="type">String</span>]): <span class="type">ZIO</span>[<span class="type">ZEnv</span>, <span class="type">Nothing</span>, <span class="type">ExitCode</span>] =</span><br><span class="line"> effect(args).map(_ => <span class="type">ExitCode</span>.success).catchAll { ex =></span><br><span class="line"> <span class="keyword">for</span> {</span><br><span class="line"> _ <- putStrLnErr(<span class="string">s"Error <span class="subst">${ex}</span>"</span>).orDie</span><br><span class="line"> } <span class="keyword">yield</span> <span class="type">ExitCode</span>.failure</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Running can be done using <code>scala-cli run Declined.scala</code></p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$> scala-cli run Declined.scala -- --version</span><br><span class="line">0.1.0</span><br></pre></td></tr></table></figure>
<p>Or by packaging the app and running the generated exe.</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">scala-cli package --jvm 14 Declined.scala</span><br><span class="line">Wrote /Users/mkocher/path/to/DeclinedApp, run it with</span><br><span class="line"> ./DeclinedApp</span><br></pre></td></tr></table></figure>
<p>Running</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">$> ./DeclinedApp --<span class="built_in">help</span></span><br><span class="line">Usage: declined [--user <string> [--alpha <floating-point>] [--fail]]</span><br><span class="line"></span><br><span class="line">Testing decline+zio</span><br><span class="line"></span><br><span class="line">Options and flags:</span><br><span class="line"> --<span class="built_in">help</span></span><br><span class="line"> Display this <span class="built_in">help</span> text.</span><br><span class="line"> --version, -v</span><br><span class="line"> Show version and Exit</span><br><span class="line"> --user <string>, -u <string></span><br><span class="line"> User name</span><br><span class="line"> --alpha <floating-point>, -a <floating-point></span><br><span class="line"> Alpha Filtering</span><br><span class="line"> --fail</span><br><span class="line"> Manually trigger a failure</span><br></pre></td></tr></table></figure>
<p>And a few smoke tests.</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$> ./DeclinedApp --user Dave --alpha 3.14</span><br><span class="line">Hello Dave. Running with alpha=3.14</span><br><span class="line">$> <span class="built_in">echo</span> $?</span><br><span class="line">0</span><br><span class="line">$ ./DeclinedApp --user Dave --alpha dragon</span><br><span class="line">Error java.lang.Exception: Invalid floating-point: dragon</span><br><span class="line">$> ./DeclinedApp --user Dave --alpha 3.14 --dragon</span><br><span class="line">Error java.lang.Exception: Unexpected option: --dragon</span><br><span class="line">$><span class="built_in">echo</span> $?</span><br><span class="line">1</span><br></pre></td></tr></table></figure>
<h2 id="Summary-and-Final-Comments"><a href="#Summary-and-Final-Comments" class="headerlink" title="Summary and Final Comments"></a>Summary and Final Comments</h2><ul>
<li><code>scala-cli</code> is a <strong>very</strong> promising addition for the Scala community.</li>
<li><code>scala-cli</code> changed my workflow. This new workflow was closer to how I would work in Python.</li>
<li>I really like ZIO’s core composablity ethos, however, it does have a learning curve.</li>
<li><code>Decline</code>‘s <code>Command[T]</code> design allows for intergrate with ZIO pretty seemlessly. </li>
<li>Misc “scrappy” CLI tools that I would typically write in Python, I could easily write in Scala.</li>
</ul>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/09/19/Exploring-TypedDict-in-Python-3-8/" >
Exploring TypedDict in Python 3.8
</a>
</h3>
<div class="article-info">
<a href="/2019/09/19/Exploring-TypedDict-in-Python-3-8/"><span class="article-date">
2019-09-19
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/mypy/" rel="tag">mypy</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p>This post will explore the new <code>TypedDict</code> feature in Python and explore leveraging <code>TypedDict</code> combined with the static analysis tool <a target="_blank" rel="noopener" href="https://github.com/python/mypy"><code>mypy</code></a> to improve the robustness of your Python code.</p>
<h2 id="PEP-589"><a href="#PEP-589" class="headerlink" title="PEP-589"></a>PEP-589</h2><p><code>TypedDict</code> was proposed in <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0589/">PEP-589</a> and accepted in Python 3.8.</p>
<p>A few key quotes from PEP-589 can provide context and motivation for the problem that <code>TypedDict</code> is attempting to address.</p>
<blockquote>
<p>This PEP proposes a type constructor typing.TypedDict to support the use case where a dictionary object has a specific set of string keys, each with a value of a specific type.</p>
</blockquote>
<blockquote>
<p>More generally, representing pure data objects using only Python primitive types such as dictionaries, strings and lists has had certain appeal. They are are easy to serialize and deserialize even when not using JSON. They trivially support various useful operations with no extra effort, including pretty-printing (through str() and the pprint module), iteration, and equality comparisons.</p>
</blockquote>
<p>This particular section of the PEP is interesting and suggests that <code>TypedDict</code> can be particularly useful for retrofitting legacy code (with type annotations). </p>
<blockquote>
<p>Dataclasses are a more recent alternative to solve this use case, but there is still a lot of existing code that was written before dataclasses became available, especially in large existing codebases where type hinting and checking has proven to be helpful. Unlike dictionary objects, dataclasses don’t directly support JSON serialization, though there is a third-party package that implements it</p>
</blockquote>
<p>The reference implementation was defined in <a target="_blank" rel="noopener" href="https://github.com/python/mypy_extensions">mypy_extensions</a> and can be installed in Python 3.7 (e.g., <code>pip install mypy_extensions</code>), or using <code>typing.TypedDict</code> in Python 3.8. </p>
<p>These following examples are run with <code>mypy</code> 0.711 and examples shown below can be obtained from <a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/e123aa0613242715717d291f9df7afdd">this gist</a>.</p>
<h2 id="Motivation-Dictionary-Mania"><a href="#Motivation-Dictionary-Mania" class="headerlink" title="Motivation: Dictionary-Mania"></a>Motivation: Dictionary-Mania</h2><p>Here’s a common example where a type-checking tool (e.g., <code>mypy</code>) won’t be able to help you catch type errors in your code.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">example_0</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="string">"""Simple Example of Using raw dict and how mypy won't catch</span></span><br><span class="line"><span class="string"> these errors with the keys</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"></span><br><span class="line"> m = <span class="built_in">dict</span>(name=<span class="string">'Star Wars'</span>, year=<span class="number">1234</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># mypy will NOT catch this error</span></span><br><span class="line"> t = m[<span class="string">'name'</span>] + <span class="number">100</span></span><br></pre></td></tr></table></figure>
<p>However, with <code>TypedDict</code>, you can define this a structural-typing-ish interface to <code>dict</code> for a specific data model.</p>
<p>Using Python < 3.8 will require <code>from mypy_extensions import TypedDict</code> whereas, Python >= 3.8 will require <code>from typing import TypedDict</code>.</p>
<p>Let’s create a simple <code>Movie</code> data model example and explore how <code>mypy</code> can be used to help catch type errors.</p>
<h2 id="Example-1-Basic-Usage-of-TypedDict"><a href="#Example-1-Basic-Usage-of-TypedDict" class="headerlink" title="Example 1: Basic Usage of TypedDict"></a>Example 1: Basic Usage of TypedDict</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Movie</span>(<span class="title class_ inherited__">TypedDict</span>):</span><br><span class="line"> name: <span class="built_in">str</span></span><br><span class="line"> year: <span class="built_in">int</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">example_01</span>():</span><br><span class="line"> m = Movie(name=<span class="string">'Star Wars'</span>, year=<span class="number">1977</span>)</span><br><span class="line"> <span class="comment"># or</span></span><br><span class="line"> m2:Movie = <span class="built_in">dict</span>(name=<span class="string">'Star Wars'</span>, year=<span class="number">1977</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># the type checker will catch this</span></span><br><span class="line"> n = m[<span class="string">'name'</span>] + <span class="number">100</span></span><br></pre></td></tr></table></figure>
<p>To enable runnable code that <strong>purposely has errors</strong> that can be caught by <code>mypy</code>, let’s define a helper function to require a specific <code>Exception</code> type to be raised. </p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> logging</span><br><span class="line">log = logging.getLogger(__name__)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">log_expected_error</span>(<span class="params">ex, fx</span>):</span><br><span class="line"> raised_error = <span class="literal">False</span></span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="keyword">return</span> fx()</span><br><span class="line"> <span class="keyword">except</span> ex <span class="keyword">as</span> e:</span><br><span class="line"> raised_error = <span class="literal">True</span></span><br><span class="line"> log.info(<span class="string">f"Got Expected error `<span class="subst">{e}</span>` of type <span class="subst">{ex}</span> from <span class="subst">{fx.__name__}</span>"</span>)</span><br><span class="line"> <span class="keyword">finally</span>:</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> raised_error:</span><br><span class="line"> log.error(<span class="string">f"Expected <span class="subst">{fx}</span> to raise <span class="subst">{ex}</span>"</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<h2 id="Example-2-Exploring-Mutating-and-Assignment-of-TypedDicts"><a href="#Example-2-Exploring-Mutating-and-Assignment-of-TypedDicts" class="headerlink" title="Example 2: Exploring Mutating and Assignment of TypedDicts"></a>Example 2: Exploring Mutating and Assignment of TypedDicts</h2><p>Let’s mutate the <code>Movie</code> <code>TypedDict</code> instance and explore how <code>mypy</code> can catch type errors during assignment.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">example_02</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> m = Movie(name=<span class="string">'Star Wars'</span>, year=<span class="number">1977</span>)</span><br><span class="line"> log.info(m)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># mypy will catch this</span></span><br><span class="line"> m[<span class="string">'name'</span>] = <span class="number">11111</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> m[<span class="string">'year'</span>] = m[<span class="string">'year'</span>] + <span class="string">'asfdsasdf'</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> log_expected_error(TypeError, f)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Use dict methods to mutate</span></span><br><span class="line"> <span class="comment"># Note, current verison of mypy is confused</span></span><br><span class="line"> <span class="comment"># by this and generates `"Movie" has no attribute "clear"`</span></span><br><span class="line"> m.clear()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f2</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="comment"># mypy won't catch this KeyError from .clear()</span></span><br><span class="line"> <span class="keyword">return</span> m[<span class="string">'year'</span>] + <span class="number">100</span></span><br><span class="line"></span><br><span class="line"> log_expected_error(KeyError, f2)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Can we mix Movie and a raw dict?</span></span><br><span class="line"> d2 = <span class="built_in">dict</span>(extras=<span class="literal">True</span>, alpha=<span class="number">1234</span>, name=<span class="number">12345</span>, year=<span class="string">'1978'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># mypy will raise TypeError here</span></span><br><span class="line"> m.update(d2)</span><br><span class="line"></span><br><span class="line"> log.info(m)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Update a Movie with a Movie</span></span><br><span class="line"> m2 = Movie(name=<span class="string">'Star Wars'</span>, year=<span class="number">1977</span>)</span><br><span class="line"> new_m = Movie(name=<span class="string">'Movie Title'</span>, year=<span class="number">1234</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Both of these are proper Movie TypedDict</span></span><br><span class="line"> <span class="comment"># hence, no mypy type error</span></span><br><span class="line"> m.update(new_m)</span><br><span class="line"> log.info(m2)</span><br></pre></td></tr></table></figure>
<p>There’s a few interesting items to note.</p>
<ul>
<li><code>mypy</code> will catch assignment errors </li>
<li>The current version of <code>mypy</code> will get a bit confused with <code>dict</code> methods, such as <code>.clear()</code>. Moreover, <code>.clear()</code> will also yield <code>KeyError</code>s (related, see <code>total=False</code> keyword of the <code>TypedDict</code>)</li>
<li><code>mypy</code> will only allow merging dicts that are the same type. You can’t mix <code>TypedDict</code> and a raw dict without <code>mypy</code> raising an issue</li>
</ul>
<h2 id="Example-3-TypedDicts-total-Keyword-Argument"><a href="#Example-3-TypedDicts-total-Keyword-Argument" class="headerlink" title="Example #3: TypedDicts total Keyword Argument"></a>Example #3: TypedDicts total Keyword Argument</h2><p>There’s a <code>total</code> keyword to the <code>TypedDict</code> that communicates that the dict does not need to be completely well formed. This is particularly interesting in how the <code>mypy</code> interpets the types. </p>
<p>For example, <code>X</code> with <code>alpha</code>, <code>beta</code> and <code>gamma</code> as <code>int</code>s, will be</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">X</span>(TypedDict, total=<span class="literal">False</span>):</span><br><span class="line"> alpha:<span class="built_in">int</span></span><br><span class="line"> beta:<span class="built_in">int</span></span><br><span class="line"> gamma:<span class="built_in">int</span></span><br><span class="line"></span><br><span class="line">x:X = <span class="built_in">dict</span>()</span><br><span class="line">x[<span class="string">'alpha'</span>] = <span class="number">1</span></span><br><span class="line">x[<span class="string">'beta'</span>] = <span class="number">2</span></span><br><span class="line">x[<span class="string">'gamma'</span>] = <span class="number">3</span></span><br></pre></td></tr></table></figure>
<p>Lets dive deeper using a variation of the previously defined <code>Movie</code> example using <code>total=False</code> to explore how <code>mypy</code> interprets the ‘incomplete’ data model.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Movie2</span>(TypedDict, total=<span class="literal">False</span>):</span><br><span class="line"> name:<span class="built_in">str</span></span><br><span class="line"> year:<span class="built_in">int</span></span><br><span class="line"> release_year: <span class="type">Optional</span>[<span class="built_in">int</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">example_03</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> Explore with defining an 'incomplete' Movie data model and how</span></span><br><span class="line"><span class="string"> None/Null checking works with mypy</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"></span><br><span class="line"> m = Movie2(name=<span class="string">'Star Wars'</span>)</span><br><span class="line"> log.info(m)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="comment"># mypy will catch this</span></span><br><span class="line"> m[<span class="string">'name'</span>] = <span class="number">1234</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># Use dict methods to mutate</span></span><br><span class="line"> <span class="comment"># mypy is confused by this. The error is:</span></span><br><span class="line"> <span class="comment"># `"Movie" has no attribute "clear"`</span></span><br><span class="line"> m.clear()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f2</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="comment"># mypy doesn't catch this NPE</span></span><br><span class="line"> <span class="comment"># I don't think it treats the type</span></span><br><span class="line"> <span class="comment"># as Optional[int]</span></span><br><span class="line"> m[<span class="string">'year'</span>] = m[<span class="string">'year'</span>] + <span class="number">100</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> log_expected_error(KeyError, f2)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Explicit test with release_year which</span></span><br><span class="line"> <span class="comment"># is fundamentally Optional[int]</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f3</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="comment"># mypy WILL catch this NPE</span></span><br><span class="line"> m[<span class="string">'release_year'</span>] = m[<span class="string">'release_year'</span>] + <span class="number">100</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> log_expected_error(KeyError, f3)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment"># This works as expected and</span></span><br><span class="line"> m2 = Movie2(name=<span class="string">'Star Wars'</span>, release_year=<span class="number">2049</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># This works as expected and mypy won't raise an error</span></span><br><span class="line"> <span class="keyword">if</span> m2[<span class="string">'release_year'</span>] <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> t = m2[<span class="string">'release_year'</span>] + <span class="number">10</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>Finally, let’s explore how <code>isinstance</code> works with <code>TypedDict</code></p>
<h2 id="Example-4-TypedDict-and-isinstance"><a href="#Example-4-TypedDict-and-isinstance" class="headerlink" title="Example 4: TypedDict and isinstance"></a>Example 4: TypedDict and isinstance</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">example_04</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> <span class="string">"""Testing isinstance"""</span></span><br><span class="line"></span><br><span class="line"> m = Movie(name=<span class="string">'Movie'</span>, year=<span class="number">1234</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> is_true = <span class="built_in">isinstance</span>(m, <span class="built_in">dict</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment"># This is a bit unexpected that this</span></span><br><span class="line"> <span class="comment"># will raise an exception at runtime</span></span><br><span class="line"> <span class="comment"># ` Cannot use isinstance() with a TypedDict type`</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">f2</span>() -> <span class="built_in">int</span>:</span><br><span class="line"> is_true = <span class="built_in">isinstance</span>(m, Movie)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> log_expected_error(TypeError, f2)</span><br></pre></td></tr></table></figure>
<p>The important item to note here is that you can NOT use <code>isinstance</code> with <code>TypedDict</code>. Python will raise a runtime error of <code>TypeError</code>. Specifically the error you’ll see is show below.</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TypeError: TypedDict does not support instance and class checks</span><br></pre></td></tr></table></figure>
<h1 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h1><ul>
<li><code>TypedDict</code> + <code>mypy</code> can be valuable to help catch type errors in Python and can help with heavy dictionary-centric interfaces in your application or library</li>
<li><code>TypedDict</code> can be used in Python 3.7 using <code>mypy_extensions</code> package</li>
<li><code>TypedDict</code> can be used in Python 2.7 using <code>mypy_extensions</code> and the 2.7 ‘namedtuple-esque’ syntax style (e.g., <code>Movie = TypedDict('Movie', {'title':str, year:int})</code>)</li>
<li>Using the <code>total=False</code> keyword to <code>TypedDict</code> can introduce large wholes the static typechecking process yielding <code>KeyError</code>s. The keyword <code>total=False</code> should be used judiciously (if at all)</li>
<li><code>isinstance</code> should <strong>not</strong> be used with <code>TypedDict</code> as it will raise a runtime <code>TypeError</code> exception</li>
<li>Be mindful when using <code>TypeDict</code> methods such as <code>clear()</code></li>
<li><code>TypeDict</code> introduces a new (somewhat) competing data modeling alternative to <a target="_blank" rel="noopener" href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a>, <a target="_blank" rel="noopener" href="https://docs.python.org/3/library/typing.html#typing.NamedTuple">typing.NamedTuple</a>, “classic” classes and third-party libraries, such as <a target="_blank" rel="noopener" href="https://github.com/samuelcolvin/pydantic">pydantic</a> and <a target="_blank" rel="noopener" href="https://github.com/python-attrs/attrs">attrs</a>. It’s not completely clear to me how all these different competing data model abstractions models are going to age gracefully</li>
</ul>
<p>I believe <code>TypedDict</code> can be a value tool to help improve clarity of interfaces, specifically in legacy code that is a bit <strong>dictionary-mania</strong> heavy. However, for new code, I would suggest avoid using <code>TypedDict</code> in favor of the thin data models, such as <a target="_blank" rel="noopener" href="https://github.com/samuelcolvin/pydantic">pydantic</a> and <a target="_blank" rel="noopener" href="https://github.com/python-attrs/attrs">attrs</a>. </p>
<p>Best to you and your Python-ing. </p>
<p>P.S. A runnable form of the code used in the post can be found in this <a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/e123aa0613242715717d291f9df7afdd">gist</a>.</p>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/06/21/Python-Dashboards-With-Panel-Kicking-the-Tires/" >
Python Dashboards with Panel: Kicking the Tires
</a>
</h3>
<div class="article-info">
<a href="/2019/06/21/Python-Dashboards-With-Panel-Kicking-the-Tires/"><span class="article-date">
2019-06-21
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p><a target="_blank" rel="noopener" href="https://pyviz.org/">PyViz</a> recently release the first official release (0.60) of <a target="_blank" rel="noopener" href="https://panel.pyviz.org/">Panel</a>. Overall, I’m digging the iterative development model of developing dashboard components within Juypter lab/notebook.</p>
<p>Here’s an example notebook that will demonstrate creating a few dashboard components in Panel. The raw notebook can be launched using <a target="_blank" rel="noopener" href="https://mybinder.org/v2/gist/mpkocher/83ac8f99a7b6a4ed87f1f2014ef74c2b/master?filepath=panel-dashboard.ipynb">mybinder.org</a>)</p>
<script src="//gist.github.com/83ac8f99a7b6a4ed87f1f2014ef74c2b.js?file=panel-dashboard.ipynb"></script>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/06/06/Python-3-8-0b1-Quick-Look/" >
Python 3.8.0b1 Positional-Only Arguments and the Walrus Operator
</a>
</h3>
<div class="article-info">
<a href="/2019/06/06/Python-3-8-0b1-Quick-Look/"><span class="article-date">
2019-06-06
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p>On June 4th 2019, <a target="_blank" rel="noopener" href="https://github.com/python/cpython/releases/tag/v3.8.0b1">Python 3.8.0b1 was released</a>. The official <a target="_blank" rel="noopener" href="https://docs.python.org/3.8/whatsnew/changelog.html#changelog">changelog is here</a>. </p>
<p>There are two interesting syntactic changes/features that were added which I believe are useful to explore in some depth. Specifically, the new “walrus”‘ <code>:=</code> operator and the new Positional-Only function parameter features.</p>
<h2 id="Walrus"><a href="#Walrus" class="headerlink" title="Walrus"></a>Walrus</h2><p>First, the “walrus” expression operator (<code>:=</code>) defined in <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0572/">PEP-572</a></p>
<blockquote>
<p>…naming sub-parts of a large expression can assist an interactive debugger, providing useful display hooks and partial results. Without a way to capture sub-expressions inline, this would require refactoring of the original code; with assignment expressions, this merely requires the insertion of a few name := markers. Removing the need to refactor reduces the likelihood that the code be inadvertently changed as part of debugging (a common cause of Heisenbugs), and is easier to dictate to another programmer.</p>
</blockquote>
<p>A (contrived) example using Python 3.8.0b1 built from source “3.8.0b1+ (heads/3.8:23f41a64ea)”</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span>xs = <span class="built_in">list</span>(<span class="built_in">range</span>(<span class="number">0</span>, <span class="number">10</span>))</span><br><span class="line"><span class="meta">>>> </span>xs</span><br><span class="line">[<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>, <span class="number">9</span>]</span><br><span class="line"><span class="meta">>>> </span>[(x, y) <span class="keyword">for</span> x <span class="keyword">in</span> xs <span class="keyword">if</span> (y := x * <span class="number">2</span>) < <span class="number">5</span>]</span><br><span class="line">[(<span class="number">0</span>, <span class="number">0</span>), (<span class="number">1</span>, <span class="number">2</span>), (<span class="number">2</span>, <span class="number">4</span>)]</span><br></pre></td></tr></table></figure>
<p>The idea is to use an expression based approach to remove unnecessary chatter and potential bugs of storing local state.</p>
<p>Another simple example:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="comment"># Python 3.8</span></span><br><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> re</span><br><span class="line"><span class="meta">>>> </span>rx = re.<span class="built_in">compile</span>(<span class="string">r'([A-z]*)\.([A-z]*)'</span>)</span><br><span class="line"><span class="meta">>>> </span><span class="keyword">def</span> <span class="title function_">g</span>(<span class="params">first, last</span>): <span class="keyword">return</span> <span class="string">f"First: <span class="subst">{first}</span> Last: <span class="subst">{last}</span>"</span></span><br><span class="line"><span class="meta">>>> </span>names = [<span class="string">'ralph'</span>, <span class="string">'steve.smith'</span>]</span><br><span class="line"><span class="meta">>>> </span><span class="keyword">for</span> name <span class="keyword">in</span> names:</span><br><span class="line"><span class="meta">... </span> <span class="keyword">if</span> (match := rx.match(name)): <span class="built_in">print</span>(g(*match.groups()))</span><br><span class="line">First: steve Last: smith</span><br></pre></td></tr></table></figure>
<p>As a side note, many of these “None-ish” based examples in the PEP (somewhat mechanically) look like a <code>map</code>, <code>flatMap</code>, <code>'foreach'</code> on <code>Option[T]</code> cases in Scala. </p>
<p>Python doesn’t really do this well due to its inside-out nature of composing maps/filter/generators (versus a left to right model). Nevertheless, here’s the example to demonstrate the general idea using a functional centric approach.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">def</span> <span class="title function_">processor</span>(<span class="params">sx</span>): <span class="keyword">return</span> rx.match(sx)</span><br><span class="line"><span class="meta">>>> </span><span class="keyword">def</span> <span class="title function_">not_none</span>(<span class="params">x</span>): <span class="keyword">return</span> x <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span></span><br><span class="line"><span class="meta">>>> </span><span class="keyword">def</span> <span class="title function_">printer</span>(<span class="params">x</span>): <span class="built_in">print</span>(g(*x.groups()))</span><br><span class="line"><span class="meta">>>> </span>_ = <span class="built_in">list</span>(<span class="built_in">map</span>(printer, <span class="built_in">filter</span>(not_none, <span class="built_in">map</span>(processor, names))))</span><br><span class="line">First: steve Last: smith</span><br></pre></td></tr></table></figure>
<p>The “Exceptional cases” described in the PEP are <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0572/#exceptional-cases">worth investigating in more detail</a>. There’s several cases where “Valid, though probably confusing” is used.</p>
<p>For example:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">y := f(x) <span class="comment"># INVALID</span></span><br><span class="line">(y := f(x)) <span class="comment"># Valid, though not recommended</span></span><br></pre></td></tr></table></figure>
<p>Note that the “walrus” operator can also be used in function definitions. </p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">foo</span>(<span class="params">answer = p := <span class="number">42</span></span>): <span class="keyword">return</span> <span class="string">""</span> <span class="comment"># INVALID</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">foo</span>(<span class="params">answer=(<span class="params">p := <span class="number">42</span></span>)</span>): <span class="keyword">return</span> <span class="string">""</span> <span class="comment"># Valid, though not great style</span></span><br></pre></td></tr></table></figure>
<h2 id="Positional-Only-Args"><a href="#Positional-Only-Args" class="headerlink" title="Positional Only Args"></a>Positional Only Args</h2><p>The other interesting feature added to Python 3.8 is Positional-Only arguments in function definitions.</p>
<p>For as long as I can recall, Python has had this fundamental feature (or bug) on how functions or methods are called. </p>
<p>For example, </p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">f</span>(<span class="params">x, y=<span class="number">1234</span></span>): <span class="keyword">return</span> x + y</span><br><span class="line"><span class="meta">>>> </span>f(<span class="number">1</span>)</span><br><span class="line"><span class="number">1235</span></span><br><span class="line"><span class="meta">>>> </span>f(<span class="number">1</span>, <span class="number">2</span>)</span><br><span class="line"><span class="number">3</span></span><br><span class="line"><span class="meta">>>> </span>f(x=<span class="number">1</span>, y=<span class="number">2</span>)</span><br><span class="line"><span class="number">3</span></span><br><span class="line"><span class="meta">>>> </span>f(y=<span class="number">1</span>, x=<span class="number">2</span>)</span><br><span class="line"><span class="number">3</span></span><br></pre></td></tr></table></figure>
<p>Often this fundamental ambiguity of function call “style” isn’t really a big deal. However, it can leak local variable names as part of the public interface.As a result, minor variable renaming can potentially break interfaces. It’s also not clear what should be a keyword only argument or a positional only argument with a default. For example, simply changing <code>f(x, y=1234)</code> to <code>f(n, y=1234)</code> can potentially break the interface depending on the call “style”.</p>
<p>I’ve worked with a few developers over the years that viewed this as a feature and thought that this style made the API calls more explicit. For example:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">compute</span>(<span class="params">alpha, beta, gamma</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">compute(alpha=<span class="number">90</span>, gamma=<span class="number">80</span>, beta=<span class="number">120</span>)</span><br></pre></td></tr></table></figure>
<p>I never really liked this looseness of positional vs keyword and would (if possible) try to discourage its use. Regardless, it can be argued this is a feature of the language (at least in Python <= 3.7). I would guess that many Python developers are also leveraging the unpacking dict style as well to call functions.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">d = <span class="built_in">dict</span>(alpha=<span class="number">90</span>, gamma=<span class="number">70</span>, beta=<span class="number">120</span>)</span><br><span class="line">compute(**d)</span><br></pre></td></tr></table></figure>
<p>In Python 3.0, function definitions using Keyword-Only arguments was added (see <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-3102/">PEP-3102</a> from 2006) using the <code>*</code> delimiter. All arguments to the right of the <code>*</code> are Keyword-Only arguments. </p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">f</span>(<span class="params">a, b, *, c=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br></pre></td></tr></table></figure>
<p>Unfortunately, this still leaves a fundamental issue with clearly defining function arguments. There are three cases: Positional-Only, Positional or Keyword, and Keyword-Only. PEP-3102 only solves the Keyword-Only case and doesn’t address the other two cases. </p>
<p>Hence in Python < 3.8, there’s still a fundamental ambiguity when defining a function and how it can be called.</p>
<p>For example:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">f</span>(<span class="params">a, b=<span class="number">1</span>, *, c=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br><span class="line"></span><br><span class="line">f(<span class="number">1</span>, <span class="number">2</span>, c=<span class="number">3</span>)</span><br><span class="line">f(a=<span class="number">1</span>, b=<span class="number">2</span>, c=<span class="number">3</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>Starting with Python 3.8.0, a Positional-Only parameters mechanism was added. The details are captured in <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0570/">PEP-570</a></p>
<p>Similar to the <code>*</code> delimiter in Python 3.0.0 for Keyword-Only args), a <code>/</code> delimiter was added to clearly delineate Positional-Only (or conversely Keyword-Only args) in function or method definitions. This makes the three cases of function arguments unambigious in how they should be called.</p>
<p>Here’s a few examples:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">f0</span>(<span class="params">a, b, c=<span class="number">1234</span>, /</span>):</span><br><span class="line"> <span class="comment"># a, b, c are only positional args. </span></span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">f1</span>(<span class="params">a, b, /, c=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="comment"># a, b must be a positional,</span></span><br><span class="line"> <span class="comment"># b can be positional or keyword</span></span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">f2</span>(<span class="params">a, b, *, c=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="comment"># a, b can be keyword or positional</span></span><br><span class="line"> <span class="comment"># but c MUST be keyword</span></span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">f3</span>(<span class="params">a, b, /, *, c=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="comment"># a, b only positional</span></span><br><span class="line"> <span class="comment"># c only keyword</span></span><br><span class="line"> <span class="comment"># e.g., # f3(1, 2, c=3)</span></span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br></pre></td></tr></table></figure>
<p>Combining the <code>/</code> and <code>*</code> with the type annotations yields:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">f4</span>(<span class="params">a:<span class="built_in">int</span>, b:<span class="built_in">int</span>, /, *, c:<span class="built_in">int</span>=<span class="number">1234</span></span>):</span><br><span class="line"> <span class="comment"># this can only be called as</span></span><br><span class="line"> <span class="comment"># f4(1, 2, c=3)</span></span><br><span class="line"> <span class="keyword">return</span> a + b + c</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>We can dive a bit deeper and inspect the function signature via <a target="_blank" rel="noopener" href="https://docs.python.org/3.8/library/inspect.html">inspect</a>.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> inspect</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">extractor</span>(<span class="params">f</span>):</span><br><span class="line"> sx = inspect.signature(f)</span><br><span class="line"> <span class="keyword">return</span> [(v.name, v.kind, v.default) <span class="keyword">for</span> v <span class="keyword">in</span> sx.parameters.values()]</span><br></pre></td></tr></table></figure>
<p>Let’s inspect each example:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">pf</span>(<span class="params">f</span>):</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">f"Function <span class="subst">{f.__name__}</span> with type annotations <span class="subst">{f.__annotations__}</span>"</span>)</span><br><span class="line"> <span class="built_in">print</span>(extractor(f))</span><br></pre></td></tr></table></figure>
<p>Running in the REPL yeilds:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span>funcs = (f0, f1, f2, f2, f2, f4)</span><br><span class="line"><span class="meta">>>> </span>_ = <span class="built_in">list</span>(<span class="built_in">map</span>(pf, funcs))</span><br><span class="line">Function f0 <span class="keyword">with</span> <span class="built_in">type</span> annotations {}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <span class="number">1234</span>)]</span><br><span class="line">Function f1 <span class="keyword">with</span> <span class="built_in">type</span> annotations {}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <span class="number">1234</span>)]</span><br><span class="line">Function f2 <span class="keyword">with</span> <span class="built_in">type</span> annotations {}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.KEYWORD_ONLY: <span class="number">3</span>>, <span class="number">1234</span>)]</span><br><span class="line">Function f2 <span class="keyword">with</span> <span class="built_in">type</span> annotations {}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.KEYWORD_ONLY: <span class="number">3</span>>, <span class="number">1234</span>)]</span><br><span class="line">Function f2 <span class="keyword">with</span> <span class="built_in">type</span> annotations {}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_OR_KEYWORD: <span class="number">1</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.KEYWORD_ONLY: <span class="number">3</span>>, <span class="number">1234</span>)]</span><br><span class="line">Function f4 <span class="keyword">with</span> <span class="built_in">type</span> annotations {<span class="string">'a'</span>: <<span class="keyword">class</span> <span class="string">'int'</span>>, <span class="string">'b'</span>: <<span class="keyword">class</span> <span class="string">'int'</span>>, <span class="string">'c'</span>: <<span class="keyword">class</span> <span class="string">'int'</span>>}</span><br><span class="line">[(<span class="string">'a'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'b'</span>, <_ParameterKind.POSITIONAL_ONLY: <span class="number">0</span>>, <<span class="keyword">class</span> <span class="string">'inspect._empty'</span>>), (<span class="string">'c'</span>, <_ParameterKind.KEYWORD_ONLY: <span class="number">3</span>>, <span class="number">1234</span>)]</span><br></pre></td></tr></table></figure>
<p>Note, you can use <a target="_blank" rel="noopener" href="https://docs.python.org/3.8/library/inspect.html#inspect.Signature.bind">bind</a> to call the func (this must also adhere to the correct function definition of arg and keywords in the function of interest).</p>
<p>Also, it’s worth noting that both scipy and numpy have been using this <code>/</code> <a target="_blank" rel="noopener" href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.gammaln.html">style in the docs</a> for some time. </p>
<h2 id="When-Should-I-Start-Adopting-these-Features"><a href="#When-Should-I-Start-Adopting-these-Features" class="headerlink" title="When Should I Start Adopting these Features?"></a>When Should I Start Adopting these Features?</h2><p>If you’re a library developer that has packages on <a target="_blank" rel="noopener" href="https://pypi.org/">PyPi</a>, it might not be clear when it’s “safe” to start leveraging these features. I was only able to find one source of Python 3 adoption and as a result, I’m only able outline a <strong>very crude model</strong>.</p>
<p>On December 23, 2016, Python 3.6 was officially released. In the Fall of 2018, <a target="_blank" rel="noopener" href="https://www.jetbrains.com/research/python-developers-survey-2018/#python-3-adoption">JetBrains release the Python Developer Survey</a> which contains the Python 2/3 breakdown, as well as the breakdown of different versions within Python 3. As of the Fall 2018, 54% of Python 3 developers were using Python 3.6.x. Therefore, using this very crude model, if you assume that the rate of adoption of 3.6 and 3.8 are the same and if the minimum threshold of adoption of 3.8 is 54%, then you’ll need to wait approximately 2 years before starting to leverage these 3.8 features. </p>
<img src="https://github.com/mpkocher/mpkocher.github.io/blob/master/images/jetbrains_python_survey_2018.png?raw=true" alt="Jetbrains Python Survey" height="600">
<p>When you do plan to leverage these 3.8 specific features and pushing a package to the Python Package Index, then I would suggest clearly defining the Python version in the <code>setup.py</code>. For more details, see the official packaging <a href="python_requires='%3E3.5.2'">docs</a> for more details.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># setup.py</span></span><br><span class="line">setup(python_requires=<span class="string">'>=3.8'</span>)</span><br></pre></td></tr></table></figure>
<h1 id="Summary-and-Conclusion"><a href="#Summary-and-Conclusion" class="headerlink" title="Summary and Conclusion"></a>Summary and Conclusion</h1><ul>
<li>Python 3.8 added the “walrus” operator <code>:=</code> that enables results of expressions to be used</li>
<li>It’s recommended reading the <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0572/#exceptional-cases">Exceptional Cases</a> for better understanding of where to (and to not) use the <code>:=</code> operator.</li>
<li>Python 3.8 added Positional-Only function definitions using the <code>/</code> delimiter</li>
<li>Defining functions with Positional-Only arguments will require a trailing <code>/</code> in the definition. E.g., <code>def adder(n, m, /): return 0</code></li>
<li>There are changes in the standard lib to communicate. It’s not clear how locked down or backward compatible the interfaces were changes. Here’s a random example of the function signature of <a target="_blank" rel="noopener" href="https://docs.python.org/3.8/library/functools.html#functools.partial">functools.partial</a> being updated to use <code>/</code>.</li>
<li>Positional-Only arguments should improve consistency of API calls across Python runtimes (e.g., cpython and pypi)</li>
<li>The Positional-Only PEP-570 outlines improvements in performance, however, I wasn’t able to find any performance studies on this topic.</li>
<li>Migrating to 3.8 might involve potentially breaking API changes based on the usage of <code>/</code> in the Python 3.8 standard lib</li>
<li>For core library authors of libs on pypi, I would recommend using the crude approximation (described above) of approximately 2 years away from being able to adopt the new 3.8 features</li>
<li>For <code>mypy</code> users, you might want to make sure you investigate the supported versions of Python 3.8 (<a target="_blank" rel="noopener" href="https://github.com/python/mypy/issues/6545">More Details</a> on the compatiblity matrix)</li>
</ul>
<p>I understand the general motivation to solve core friction points or ambiguities at the language level, however, the new syntatic changes are a little too noisy for my tastes. Specifically the Positional-Only <code>/</code> combined with the <code>*</code> and type annotations. Regardless, the (Python 3.8) ship has sailed long ago. I would encourage the Python community to periodically track and provide feedback on the current <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/">PEP</a>s to help guide the evolution of the Python programming language. And finally, Python 3.8.0 (beta and future 3.8 RCs) bugs should be filed to <a target="_blank" rel="noopener" href="https://bugs.python.org/">https://bugs.python.org</a> . </p>
<p>Best to you and your Python-ing!</p>
<h2 id="Further-Reading"><a href="#Further-Reading" class="headerlink" title="Further Reading"></a>Further Reading</h2><ul>
<li><a target="_blank" rel="noopener" href="https://www.python.org/downloads/release/python-380b1/">Full Python 3.8.0b1</a> release notes</li>
<li><a target="_blank" rel="noopener" href="https://news.ycombinator.com/item?id=19690152">Hacker News</a> discussion on Positional-Only feature</li>
<li><a target="_blank" rel="noopener" href="https://medium.com/hultner/try-out-walrus-operator-in-python-3-8-d030ce0ce601">Alexander Hutner</a> covering walrus feature</li>
<li><a target="_blank" rel="noopener" href="https://bugs.python.org/">Bug Tracker</a></li>
<li><a target="_blank" rel="noopener" href="https://docs.python.org/3.8/whatsnew/changelog.html#changelog">Python 3.8 ChangeLog</a></li>
<li><a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/">Python PEP</a>s</li>
</ul>
<p>P.S. A reminder that the PSF has a <a target="_blank" rel="noopener" href="https://www.python.org/psf/donations/2019-q2-drive/">Q2 2019 fundraiser</a> that ends June 30th.</p>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/05/22/Dataclasses-in-Python-3-7/" >
Dataclasses in Python 3.7
</a>
</h3>
<div class="article-info">
<a href="/2019/05/22/Dataclasses-in-Python-3-7/"><span class="article-date">
2019-05-22
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p>TDLR: Initially, I was interested in the <code>dataclasses</code> feature that was added to Python 3.7 standard library. However after a closer look, It’s not clear to me if <code>dataclasses</code> provide enough value. Third-party libraries such as <a target="_blank" rel="noopener" href="https://github.com/python-attrs/attrs">attrs</a> and <a target="_blank" rel="noopener" href="https://pydantic-docs.helpmanual.io/">pydantic</a> offer considerably more value. Moreover, by adding <code>dataclasses</code> to the standard library, Python now has 3 (or 4) overlapping mechanisms (namedtuple, <code>typing.NamedTuple</code>, “classic” classes, and dataclasses) for defining a core concept in the Python programming language. </p>
<p>In Python 3.7, <a target="_blank" rel="noopener" href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a> were added to the standard library. There’s a <a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=T-TwcmT6Rcw">terrific talk by Raymond Hettinger</a> that is a great introduction to the history and design of <code>dataclasses</code>.</p>
<p>A demonstration and comparison of the 3 different models is shown below.</p>
<p>For the demo, I’ll use a <code>Person</code> data model with an id (int), a name (str), and an optional favorite color (Optional[str]). The <code>Person</code> model will also have custom <code>eq</code> and <code>hash</code> using only the <code>Person</code>s <code>id</code>. I’m a fan of immutable classes, so the container data model will be <code>immutable</code> if possible.</p>
<p>Here’s an example using the “classic” class style leveraging Python 3 type annotations.</p>
<p>Nothing particularly interesting for any Python dev. We do have to write a bit of boilerplate in the constructor, however, this also enables some convenience translation layers (e.g., converting datetime as a string to datetime instance). I’ve omitted the immutability aspect due to the boilerplate that would be added.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">Optional</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, <span class="built_in">id</span>:<span class="built_in">int</span>, name:<span class="built_in">str</span>, favorite_color:<span class="type">Optional</span>[<span class="built_in">str</span>] = <span class="literal">None</span></span>):</span><br><span class="line"> self.<span class="built_in">id</span> = <span class="built_in">id</span></span><br><span class="line"> self.name = name</span><br><span class="line"> self.favorite_color = favorite_color</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__repr__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"<{} id:{} name:{}>"</span>.<span class="built_in">format</span>(self.__class__.__name__, self.<span class="built_in">id</span>, self.name)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__hash__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">hash</span>(self.<span class="built_in">id</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line"> <span class="keyword">if</span> self.__class__ == other.__class__:</span><br><span class="line"> <span class="keyword">return</span> self.<span class="built_in">id</span> == other.<span class="built_in">id</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure>
<p>We can also write this using the <code>typing.NamedTuple</code> (or an untyped version using the Python 2 style <code>collections.namedtuple</code>).</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> NamedTuple, <span class="type">Optional</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>(<span class="title class_ inherited__">NamedTuple</span>):</span><br><span class="line"> <span class="built_in">id</span>:<span class="built_in">int</span></span><br><span class="line"> name:<span class="built_in">str</span></span><br><span class="line"> favorite_color:<span class="type">Optional</span>[<span class="built_in">str</span>] = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__hash__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">hash</span>(self.<span class="built_in">id</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line"> <span class="keyword">if</span> self.__class__ == other.__class__:</span><br><span class="line"> <span class="keyword">return</span> self.<span class="built_in">id</span> == other.<span class="built_in">id</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure>
<p>Nothing too interesting here either. This approach does have well understood downsides due to the tuple nature of the underlying design. Specific downsides include index access and comparison operators (e.g., <code>__lte__</code>) which leverage the sortablity of tuples that can potentially introduce unexpected behaviors. </p>
<p>Let’s use the new Python 3.7 <a target="_blank" rel="noopener" href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a> approach.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> dataclasses <span class="keyword">import</span> dataclass, field</span><br><span class="line"></span><br><span class="line"><span class="meta">@dataclass(<span class="params">frozen=<span class="literal">True</span></span>)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>:</span><br><span class="line"> <span class="built_in">id</span>:<span class="built_in">int</span></span><br><span class="line"> name:<span class="built_in">str</span> = field(<span class="built_in">hash</span>=<span class="literal">False</span>, compare=<span class="literal">False</span>)</span><br><span class="line"> favorite_color: <span class="type">Optional</span>[<span class="built_in">str</span>] = field(default_factory=<span class="keyword">lambda</span> :<span class="literal">None</span>, <span class="built_in">hash</span>=<span class="literal">False</span>, compare=<span class="literal">False</span>)</span><br></pre></td></tr></table></figure>
<p>The <code>dataclasses</code> design is a declarative centric approach yielding a terse and clean interface to define a class in Python. </p>
<p>It’s important to note that <strong>none of the 3 approaches in the standard lib support any mechanism for type checking at runtime</strong>. The <code>dataclasses</code> API has a <code>__post_init</code> hook that can be used to add any custom validation (typechecking or otherwise).</p>
<p>I think it’s useful to dig a bit deeper and understand the development requirements, or conversely, the non-goals of the <code>dataclasses</code> API. <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0557/#rationale">PEP-557</a> provides some key insights (for context, one of the libraries mention in the quote below is <code>attrs</code>).</p>
<p>Here’s a few useful snippets from the PEP.</p>
<blockquote>
<p>One main design goal of Data Classes is to support static type checkers. The use of PEP 526 syntax is one example of this, but so is the design of the fields() function and the @dataclass decorator. Due to their very dynamic nature, some of the libraries mentioned above are difficult to use with static type checkers.</p>
</blockquote>
<p>It’s not clear to me if these “very dynamic” libraries still have an issue with popular static analysis tools such as <code>mypy</code> (as of <code>mypy</code> 0.57, <code>attrs</code> is supported). Nevertheless, it’s very clear from the PEP that there is a strong opinion that Python is a dynamic language and adding types is to annotation functions with metadata for static analysis tools. Adding types is not for runtime type checking, nor is to imply that type signatures are required. </p>
<p>Another useful snippet to provide context.</p>
<blockquote>
<p>Data Classes are not, and are not intended to be, a replacement mechanism for all of the above libraries. But being in the standard library will allow many of the simpler use cases to instead leverage Data Classes. Many of the libraries listed have different feature sets, and will of course continue to exist and prosper.</p>
</blockquote>
<p>Historically, Python has a very heavy batteries-included approach to the standard library. To put the size of the standard library into perspective, there’s a recent <a target="_blank" rel="noopener" href="https://www.python.org/dev/peps/pep-0594/">PEP-594</a> to remove “dead batteries” from the standard library. The list of packages to remove is quite interesting. This battery-centric included approach motivates questions about the standard library. </p>
<ul>
<li><p>Does the Python standard library need to be any bigger? </p>
</li>
<li><p>Why can’t (or shouldn’t) <code>dataclasses</code> style or functionality be off-loaded to the community to develop libraries (e.g., <code>attrs</code> and <code>traitlets</code>)? Is a “half” or “minimal” solution really worth adding enough value?</p>
</li>
<li><p>Does Python standard lib need yet another competing mechanism to define a class or data container? </p>
</li>
<li><p>Is all of these packages in the standard lib really that useful in practice? For example, is the <a target="_blank" rel="noopener" href="https://docs.python.org/3/library/csv.html">CSV parser</a> used when <code>pandas.read_csv</code> exists? </p>
</li>
<li><p>Do these features in the standard library bog down the Python core team?</p>
</li>
</ul>
<p>A recent talk at Python Language Summit in May of 2019, “<a target="_blank" rel="noopener" href="http://pyfound.blogspot.com/2019/05/amber-brown-batteries-included-but.html">Batteries included, But They’re Leaking</a>“ by <a target="_blank" rel="noopener" href="https://twitter.com/hawkieowl">Amber Brown</a> brought up some contentious ideas on the current state of the standard library (in practice, that ship has sailed many moons ago and I’m not sure there’s a lot of constructive discussion that can be had on this topic).</p>
<p>I don’t really have any solid answers to any of these questions. Two of the most popular and Python libs, <code>requests</code> and <code>numpy</code> are both outside of the standard library (for good reasons that may be orthogonal to motivations of adding <code>dataclasses</code> to the standard library) and are thriving. </p>
<p>Without hooks at a per <code>field</code> level validation hooks among other features, I’m struggling to find the usefulness of <code>dataclasses</code> for Python developers, particularly in the context of current polished third-party libraries, such as <code>attrs</code>, <code>pydantic</code>, or <code>traitlets</code>. </p>
<p>For comparison, let’s take a look at the third-party libraries that have inspired <code>dataclasses</code>.</p>
<h1 id="Third-Party-Libraries"><a href="#Third-Party-Libraries" class="headerlink" title="Third Party Libraries"></a>Third Party Libraries</h1><p>Let’s take a quick look into two of the third-party libraries, <a target="_blank" rel="noopener" href="https://github.com/python-attrs/attrs">attrs</a> and <a target="_blank" rel="noopener" href="https://pydantic-docs.helpmanual.io/">pydantic</a> that inspired <code>dataclasses</code>. I believe both of these libraries are supported by <code>mypy</code>.</p>
<h2 id="Attrs"><a href="#Attrs" class="headerlink" title="Attrs"></a>Attrs</h2><p>Similar to the <code>dataclasses</code> approach, the types in <code>attrs' </code>are only used as annotations (e.g., <code>__annotations__</code>) and are not enforced at runtime. However, the general validation mechanism trivially enables runtime type validation. For the purposes of this example, let’s also add custom validation on the <code>name</code> of the <code>Person</code> as well as adding type checking.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">Optional</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> attr</span><br><span class="line"><span class="keyword">from</span> attr.validators <span class="keyword">import</span> instance_of <span class="keyword">as</span> vof</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_positive_nonzero</span>(<span class="params">instance, attribute, value</span>):</span><br><span class="line"> <span class="keyword">if</span> value < <span class="number">1</span>:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">f"Value <span class="subst">{value}</span> must be >0"</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">check_non_empty_str</span>(<span class="params">inst, attribute, value</span>):</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> value:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">"name must be a non-empty string"</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@attr.s(<span class="params">auto_attribs=<span class="literal">True</span>, frozen=<span class="literal">True</span></span>)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>:</span><br><span class="line"> <span class="built_in">id</span>:<span class="built_in">int</span> = attr.ib(validator=[vof(<span class="built_in">int</span>), is_positive_nonzero])</span><br><span class="line"> name:<span class="built_in">str</span> = attr.ib(validator=[vof(<span class="built_in">str</span>), check_non_empty_str])</span><br><span class="line"> favorite_color: <span class="type">Optional</span>[<span class="built_in">str</span>] = attr.ib(default=<span class="literal">None</span>, validator=[vof((<span class="built_in">str</span>, <span class="built_in">type</span>(<span class="literal">None</span>)))])</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__hash__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">hash</span>(self.<span class="built_in">id</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line"> <span class="keyword">if</span> self.__class__ == other.__class__:</span><br><span class="line"> <span class="keyword">return</span> self.<span class="built_in">id</span> == other.<span class="built_in">id</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure>
<p>The <a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=T-TwcmT6Rcw">abstract of Raymond Hettinger’s talk from Pycon</a> has an interesting summary of <code>dataclasses</code>.</p>
<blockquote>
<p>Dataclasses are shown to be the next step in a progression of data aggregation tools: tuple, dict, simple class, bunch recipe, named tuples, records, attrs, and then dataclasses. Each builds upon the one that came before, adding expressiveness at the expense of complexity.</p>
</blockquote>
<p>I’m not sure I completely agree. The <code>dataclasses</code> implementation looks closer to <code>attrs</code>-lite, than the “next step of progression”. </p>
<h2 id="Pydantic"><a href="#Pydantic" class="headerlink" title="Pydantic"></a>Pydantic</h2><p>Another alternative is <a target="_blank" rel="noopener" href="https://pydantic-docs.helpmanual.io/">pydantic</a>. This is a bit more opinionated design. It also has a nice <code>Schema</code> abstraction to communicate core metadata on the fields as well as first class support for serialization hooks. The <code>pydantic</code> library also has a <code>dataclasses</code> wrapper layer that can be accessed via <code>pydantic.dataclasses</code>.</p>
<p>Here’s an example of defining our <code>Person</code> data model.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">Optional</span></span><br><span class="line"><span class="keyword">import</span> pydantic</span><br><span class="line"><span class="keyword">from</span> pydantic <span class="keyword">import</span> BaseModel, validator, PositiveInt</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>(<span class="title class_ inherited__">BaseModel</span>):</span><br><span class="line"> <span class="keyword">class</span> <span class="title class_">Config</span>:</span><br><span class="line"> allow_mutation = <span class="literal">False</span></span><br><span class="line"> validate_all = <span class="literal">True</span> </span><br><span class="line"></span><br><span class="line"> <span class="built_in">id</span>:PositiveInt</span><br><span class="line"> name:<span class="built_in">str</span></span><br><span class="line"> favorite_color: <span class="type">Optional</span>[<span class="built_in">str</span>] = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="meta"> @validator(<span class="params"><span class="string">'name'</span></span>)</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">validate_name</span>(<span class="params">cls, v</span>):</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> v:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">"Name can't be an empty"</span>)</span><br><span class="line"> <span class="keyword">return</span> v</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__hash__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">hash</span>(self.<span class="built_in">id</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line"> <span class="keyword">if</span> self.__class__ == other.__class__:</span><br><span class="line"> <span class="keyword">return</span> self.<span class="built_in">id</span> == other.<span class="built_in">id</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure>
<p>Overall, I like the general style and approach, however, it does have a few quarks. Specifically the keyword only usage as well as unexpected casting behavior of <code>int</code>s to <code>str</code>s.</p>
<p>The <code>pydantic</code> API also supports rich metadata that could be useful for generating commandline interfaces for a given schema data model and emitted JSONSchema.</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pydantic <span class="keyword">import</span> BaseModel, validator, ValidationError, Schema</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>(<span class="title class_ inherited__">BaseModel</span>):</span><br><span class="line"> <span class="keyword">class</span> <span class="title class_">Config</span>:</span><br><span class="line"> allow_mutation = <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># '...' means the value is required.</span></span><br><span class="line"> <span class="built_in">id</span>: PositiveInt = Schema(..., title=<span class="string">"User Id"</span>)</span><br><span class="line"> name:<span class="built_in">str</span> = Schema(..., title=<span class="string">"User name"</span>)</span><br><span class="line"> favorite_color:<span class="type">Optional</span>[<span class="built_in">str</span>] = Schema(<span class="literal">None</span>,</span><br><span class="line"> title=<span class="string">"User Favorite Color"</span>,</span><br><span class="line"> description=<span class="string">"Favorite Color. Provided as case sensitive english spelling"</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta"> @validator(<span class="params"><span class="string">'name'</span></span>)</span></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">validate_name</span>(<span class="params">cls, v</span>):</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> v:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">"Name can't be an empty"</span>)</span><br><span class="line"> <span class="keyword">return</span> v</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__hash__</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">hash</span>(self.<span class="built_in">id</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line"> <span class="keyword">if</span> self.__class__ == other.__class__:</span><br><span class="line"> <span class="keyword">return</span> self.<span class="built_in">id</span> == other.<span class="built_in">id</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#Person.schema() will emit the JSONSchema as a dict.</span></span><br></pre></td></tr></table></figure>
<h1 id="Summary-And-Conclusion"><a href="#Summary-And-Conclusion" class="headerlink" title="Summary And Conclusion"></a>Summary And Conclusion</h1><ul>
<li><code>dataclasses</code> offers a terse syntax for defining a class or data container that has type annotations using a code generation approach</li>
<li><code>dataclasses</code> <code>field</code> metadata can be used to define defaults, communicate which fields should be used in <code>eq</code> or <code>hash</code>, <code>lte</code>, etc…</li>
<li><code>dataclasses</code> has a <code>__post_init</code> hook that can be used for validation</li>
<li><code>dataclasses</code> By design does not do type validation. It only adds <code>__annotation__</code> to the data container for static analyzers to consume, such as <code>mypy</code></li>
<li>Since <code>dataclasses</code> is now in the standard lib, this means feature enhancement, bug fixes, and backwards compatibility are now coupled the official Python release process</li>
<li><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=T-TwcmT6Rcw">Raymond’s Pycon</a> talk mentions the end-to-end develop time on <code>dataclasses</code> was 200+ hrs</li>
</ul>
<p>Initially, I was a intrigued by the addition of <code>dataclasses</code> to the standard library. However, after a deeper dive into the <code>dataclasses</code>, it’s not clear to me that these are particularly useful for Python developers. I believe third-party solutions such as <code>attrs</code> or <code>pydantic</code> might be a better fit due to their validation hooks and richer feature sets. It will be interesting to see the adoption of <code>dataclasses</code> by both the Python core as well as third-party developers.</p>
<p>For a deeper look and comparison of the 3 (or 4) models to define a class or data container in Python, please consult this <a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/4482e9315c64241f442b1e5f8783316d">Notebook in this Gist</a></p>
<p>Best on all your Python-ing!</p>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/03/01/Functional-Programming-Techniques-In-Python-Series/" >
Series: Functional Programming Techniques In Python
</a>
</h3>
<div class="article-info">
<a href="/2019/03/01/Functional-Programming-Techniques-In-Python-Series/"><span class="article-date">
2019-03-01
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/FPT/" rel="tag">FPT</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/functional/" rel="tag">functional</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/series/" rel="tag">series</a></li></ul>
</span>
</div>
<div class="article-entry">
<p>This is a 4 Part Series that explores functional centric design style and patterns in Python.</p>
<script src="//gist.github.com/9896022.js"></script>
<p><a href="https://mpkocher.github.io/2019/01/30/Functional-Python-Part-1/">Part 1</a> (<a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/d1948e54c7863b1548ec4639df44b954">notebook</a>) starts with different mechanisms of how to define functions in Python and quickly moves to using <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Closure_(computer_programming)">closures</a> and <code>functools.partial</code>. We then move on to adding <code>functools.reduce</code> and composition with <a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/9896022">compose</a> to our toolbox. Finally, we conclude with adding lazy <code>map</code> and <code>filter</code> to our toolbox and create a data pipeline that takes a stream of records to compute common statics using a max heap as the reducer. </p>
<p>In <a href="https://mpkocher.github.io/2019/02/02/Functional-Python-Part-2/">Part 2</a> (<a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/517d9e72536346de505bff47199a9b24">notebook</a>), we explore building a REST client using functional-centric design style. Using an iterative approach, we build up a REST client using small functions leveraging clsoures and passing functions as first class citizens to methods. To conclude, we wrap the API and expose the REST client via a simple Python class.</p>
<p><a href="https://mpkocher.github.io/2019/02/28/Functional-Python-Part-3/">Part 3</a> (<a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/4e261b38d8b1c76a7f6dd8e9a95a4873">notebook</a>) follows similarly to spirit to Part 2. We build a commandline interface leveraging <code>argparse</code> from the Python standard library. Sometimes libraries such as argparse can have rough edges or friction points in the API that introduce duplication or design issues. Part 3 focuses on iterative building up an expressive commandline interface to a subparser commandline tool using closures and functions to smooth out the rough edges of <code>argparse.</code>. There’s also examples of using a Strategy-ish pattern with type annotated functions to enable configuring logging as well as custom error handling. </p>
<p><a href="https://mpkocher.github.io/2019/03/01/Functional-Python-Part-4/">Part 4</a> (<a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/88f01a6237df44a8a01b51fb58cbb544">notebook</a>) concludes with some gotchas with regards to scope in closures, a brief example of decorators and a few suggestions for leverging function-centric designs in your APIs or applications. </p>
<p>If you’re a OO wizard, a Data Scientist/Analysist, or a backend dev, this series can be useful to add another design approach in your toolbelt to designing APIs or programs. </p>
<p>Best to you and your Python’ing!</p>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/03/01/Functional-Python-Part-4/" >
Functional Programming Techniques in Python Part 4
</a>
</h3>
<div class="article-info">
<a href="/2019/03/01/Functional-Python-Part-4/"><span class="article-date">
2019-03-01
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/FPT/" rel="tag">FPT</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/functional/" rel="tag">functional</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p><a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/88f01a6237df44a8a01b51fb58cbb544">Functional Programming Techniques in Python Part 4</a></p>
<script src="//gist.github.com/88f01a6237df44a8a01b51fb58cbb544.js"></script>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/02/28/Functional-Python-Part-3/" >
Functional Programming Techniques in Python Part 3
</a>
</h3>
<div class="article-info">
<a href="/2019/02/28/Functional-Python-Part-3/"><span class="article-date">
2019-02-28
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/FPT/" rel="tag">FPT</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/functional/" rel="tag">functional</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p><a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/4e261b38d8b1c76a7f6dd8e9a95a4873">Functional Programming Techniques in Python Part 3</a></p>
<script src="//gist.github.com/4e261b38d8b1c76a7f6dd8e9a95a4873.js"></script>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/02/02/Functional-Python-Part-2/" >
Functional Programming Techniques in Python Part 2
</a>
</h3>
<div class="article-info">
<a href="/2019/02/02/Functional-Python-Part-2/"><span class="article-date">
2019-02-02
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/FPT/" rel="tag">FPT</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/functional/" rel="tag">functional</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p><a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/517d9e72536346de505bff47199a9b24">Functional Programming Techniques in Python Part 2</a></p>
<script src="//gist.github.com/517d9e72536346de505bff47199a9b24.js"></script>
</div>
</div>
</div>
<div class="post-list">
<div class="post-list-item article">
<h3 class="article-header">
<a href="/2019/01/30/Functional-Python-Part-1/" >
Functional Programming Techniques in Python Part 1
</a>
</h3>
<div class="article-info">
<a href="/2019/01/30/Functional-Python-Part-1/"><span class="article-date">
2019-01-30
</span>
</a>
<span class="article-tag tagcloud">
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/FPT/" rel="tag">FPT</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/functional/" rel="tag">functional</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/python/" rel="tag">python</a></li></ul>
</span>
</div>
<div class="article-entry">
<p><a target="_blank" rel="noopener" href="https://gist.github.com/mpkocher/d1948e54c7863b1548ec4639df44b954">Functional Programming Techniques in Python Part 1</a></p>
<script src="//gist.github.com/d1948e54c7863b1548ec4639df44b954.js"></script>
</div>
</div>
</div>
<div class="pagination">
<span class="page-number current">1</span><a class="page-number" href="/page/2/">2</a><a class="extend next" rel="next" href="/page/2/">Next</a>
</div>
<div class="main-footer">
© 2022 - Powered by <a href="http://hexo.io" target="_blank">Hexo</a> - Theme <a href="https://github.com/denjones/hexo-theme-chan" target="_blank">Chan</a>
</div>
</div>
</div>
</div>
<script src="//apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<link rel="stylesheet" href="/PhotoSwipe/photoswipe.css">
<link rel="stylesheet" href="/PhotoSwipe/default-skin/default-skin.css">
<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
<!-- Background of PhotoSwipe.
It's a separate element as animating opacity is faster than rgba(). -->
<div class="pswp__bg"></div>
<!-- Slides wrapper with overflow:hidden. -->
<div class="pswp__scroll-wrap">
<!-- Container that holds slides.
PhotoSwipe keeps only 3 of them in the DOM to save memory.
Don't modify these 3 pswp__item elements, data is added later on. -->
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__top-bar">
<!-- Controls are self-explanatory. Order can be changed. -->
<div class="pswp__counter"></div>
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
<button class="pswp__button pswp__button--share" title="Share"></button>
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
<!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
<!-- element will get class pswp__preloader--active when preloader is running -->
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
</div>
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>