A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 54KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>API Evolution the Right Way (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://emptysqua.re/blog/api-evolution-the-right-way/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. API Evolution the Right Way (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://emptysqua.re/blog/api-evolution-the-right-way/">Source originale du contenu</a></h3>
  445. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/dragon.jpg" alt=""/></p>
  446. <p>Imagine you are a creator deity, designing a body for a creature. In your benevelonce, you wish for the creature to evolve over time: first, because it must respond to changes in its environment, and second, because your wisdom grows and you think of better designs for the beast. It shouldn’t remain in the same body forever!</p>
  447. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/praise-the-creator.jpg" alt=""/></p>
  448. <p>The creature, however, might be relying on features of its present anatomy. You can’t add wings or change its scales without warning. It needs an orderly process to adapt its lifestyle to its new body. How can you, as a responsible designer in charge of this creature’s natural history, gently coax it toward ever greater improvements?</p>
  449. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/creator.jpg" alt=""/></p>
  450. <p>It’s the same for responsible library maintainers. We keep our promises to the people who depend on our code: we release bugfixes and useful new features. We sometimes delete features if that’s beneficial for the library’s future. We continue to innovate, but we don’t break the code of people who use our library. How can we fulfill all those goals at once?</p>
  451. <h1 id="add-useful-features">Add Useful Features</h1>
  452. <p>Your library shouldn’t stay the same for eternity: you should add features that your make your library better for your users.
  453. For example, if you have a Reptile class and it would be useful to have wings for flying, go for it.</p>
  454. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>class</span> <span>Reptile</span>:
  455. <span>@property</span>
  456. <span>def</span> <span>teeth</span>(<span>self</span>):
  457. <span>return</span> <span>'sharp fangs'</span>
  458. <span> <span># If wings are useful, add them!</span>
  459. </span><span> <span>@property</span>
  460. </span><span> <span>def</span> <span>wings</span>(<span>self</span>):
  461. </span><span> <span>return</span> <span>'majestic wings'</span>
  462. </span></code></pre></div>
  463. <p>But beware, features come with risk.
  464. Consider the following feature in the Python standard library, and let us see what went wrong with it.</p>
  465. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>bool</span>(datetime<span>.</span>time(<span>9</span>, <span>30</span>)) <span>==</span> <span>True</span>
  466. <span>bool</span>(datetime<span>.</span>time(<span>0</span>, <span>0</span>)) <span>==</span> <span>False</span>
  467. </code></pre></div>
  468. <p>This is peculiar: converting any time object to a boolean yields True, except for midnight. (Worse, the rules for timezone-aware times are even stranger.)
  469. I’ve been writing Python for more than a decade but I didn’t discover this rule until last week. What kind of bugs can this odd behavior cause in users’ code?</p>
  470. <p>Consider a calendar application with a function that creates events. If an event has an end time, the function requires it to also have a start time:</p>
  471. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>def</span> <span>create_event</span>(day,
  472. start_time<span>=</span><span>None</span>,
  473. end_time<span>=</span><span>None</span>):
  474. <span>if</span> end_time <span>and</span> <span>not</span> start_time:
  475. <span>raise</span> <span>ValueError</span>(<span>"Can't pass end_time without start_time"</span>)
  476. <span># The coven meets from midnight until 4am.</span>
  477. create_event(datetime<span>.</span>date<span>.</span>today(),
  478. datetime<span>.</span>time(<span>0</span>, <span>0</span>),
  479. datetime<span>.</span>time(<span>4</span>, <span>0</span>))
  480. </code></pre></div>
  481. <p>Unfortunately for witches, an event starting at midnight fails this validation. A careful programmer who knows about the quirk at midnight can write this function correctly, of course:</p>
  482. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>def</span> <span>create_event</span>(day,
  483. start_time<span>=</span><span>None</span>,
  484. end_time<span>=</span><span>None</span>):
  485. <span>if</span> end_time <span>is</span> <span>not</span> <span>None</span> <span>and</span> start_time <span>is</span> <span>None</span>:
  486. <span>raise</span> <span>ValueError</span>(<span>"Can't pass end_time without start_time"</span>)
  487. </code></pre></div>
  488. <p>But this subtlety is worrisome. If a library creator wanted to make an API that bites users, a “feature” like the boolean conversion of midnight works nicely.</p>
  489. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/bite.jpg" alt=""/></p>
  490. <p>The responsible creator’s goal, however, is to make your library easy to use correctly.</p>
  491. <p>This feature was written by Tim Peters when he first made the datetime module in 2002. Even founding Pythonistas like Tim make mistakes. <a href="https://bugs.python.org/issue13936">The quirk was removed</a>, and all times are True now.</p>
  492. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Python 3.5 and later.</span>
  493. <span>bool</span>(datetime<span>.</span>time(<span>9</span>, <span>30</span>)) <span>==</span> <span>True</span>
  494. <span>bool</span>(datetime<span>.</span>time(<span>0</span>, <span>0</span>)) <span>==</span> <span>True</span>
  495. </code></pre></div>
  496. <p>Programmers who didn’t know about the oddity of midnight are saved from obscure bugs, but it makes me nervous to think about any code that actually relies on the weird old behavior and didn’t notice the change. It would have been better if this bad feature were never implemented at all. This leads us to the first promise of any library maintainer:</p>
  497. <div><p>
  498. First Covenant:<br/>Avoid Bad Features
  499. </p></div>
  500. <p>The most painful change to make is when you have to delete a feature. One way to avoid bad features is to add few features in general! Make no public method, class, function, or property without a good reason. Thus:</p>
  501. <div><p>
  502. Second Covenant:<br/>Minimize Features
  503. </p></div>
  504. <p>Features are like children: conceived in a moment of passion, they must be supported for years.
  505. Don’t do anything silly just because you can. Don’t add feathers to a snake!</p>
  506. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/feathers.jpg" alt=""/></p>
  507. <p>But of course, there are plenty of occasions when users need something from your library that it does not yet offer. How do you choose the right feature to give them? Here’s another cautionary tale.</p>
  508. <h1 id="a-cautionary-tale-from-asyncio">A Cautionary Tale From asyncio</h1>
  509. <p>As you may know, when you call a coroutine function, it returns a coroutine object:</p>
  510. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>async</span> <span>def</span> <span>my_coroutine</span>():
  511. <span>pass</span>
  512. <span>print</span>(my_coroutine())
  513. </code></pre></div>
  514. <div class="highlight"><pre><code class="language-text" data-lang="text"><span/>&amp;lt;coroutine object my_coroutine at 0x10bfcbac8&amp;gt;
  515. </code></pre></div>
  516. <p>Your code must “await” this object to actually run the coroutine. It’s easy to forget this, so the asyncio developers wanted a “debug mode” that catches this mistake. Whenever a coroutine is destroyed without being awaited, the debug mode prints a warning with a traceback to the line where it was created.</p>
  517. <p>When Yury Selivanov implemented the debug mode, he added as its foundation a “coroutine wrapper” feature. The wrapper is a function that takes in a coroutine and returns anything at all. Yury used it to install the warning logic on each coroutine, but someone else could use it to turn coroutines into the string “hi!”:</p>
  518. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>import</span> <span>sys</span>
  519. <span>def</span> <span>my_wrapper</span>(coro):
  520. <span>return</span> <span>'hi!'</span>
  521. sys<span>.</span>set_coroutine_wrapper(my_wrapper)
  522. <span>async</span> <span>def</span> <span>my_coroutine</span>():
  523. <span>pass</span>
  524. <span>print</span>(my_coroutine())
  525. </code></pre></div>
  526. <p>That is one hell of a customization. It changes the very meaning of “async”. Calling set_coroutine_wrapper once will globally and permanently change all coroutine functions.
  527. It is, as Nathaniel Smith wrote, “a problematic API” which is prone to misuse and had to be removed. The asyncio developers could have avoided the pain of deleting the feature if they’d better shaped it to its purpose. Responsible creators must keep this in mind:</p>
  528. <div><p>
  529. Third Covenant:<br/>Keep Features Narrow
  530. </p></div>
  531. <p>Luckily, Yury had the good judgment to mark this feature provisional, so asyncio users knew not to rely on it. Nathaniel was free to replace set_coroutine_wrapper with a narrower feature that only customized the traceback depth:</p>
  532. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>import</span> <span>sys</span>
  533. sys<span>.</span>set_coroutine_origin_tracking_depth(<span>2</span>)
  534. <span>async</span> <span>def</span> <span>my_coroutine</span>():
  535. <span>pass</span>
  536. <span>print</span>(my_coroutine())
  537. </code></pre></div>
  538. <div class="highlight"><pre><code class="language-text" data-lang="text"><span/>&lt;coroutine object my_coroutine at 0x10bfcbac8&gt;
  539. RuntimeWarning:'my_coroutine' was never awaited
  540. Coroutine created at (most recent call last)
  541. File "script.py", line 8, in &lt;module&gt;
  542. print(my_coroutine())
  543. </code></pre></div>
  544. <p>This is much better. There’s no more global setting that can change coroutines’ type, so asyncio users need not code as defensively. Deities should all be as farsighted as Yury:</p>
  545. <div><p>
  546. Fourth Covenant:<br/>Mark Experimental Features "Provisional"
  547. </p></div>
  548. <p>If you have merely a hunch that your creature wants horns and a quadruple-forked tongue, introduce the features but mark them “provisional”.</p>
  549. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/horns.jpg" alt=""/></p>
  550. <p>You might discover that the horns are adventitious but the quadruple-forked tongue is useful after all. In the next release of your library you can delete the former and mark the latter official.</p>
  551. <h1 id="deleting-features">Deleting Features</h1>
  552. <p>No matter how wisely we guide our creature’s evolution, there may come a time when it’s best to delete an official feature. For example, you might have created a lizard, and now you choose to delete its legs. Perhaps you want to transform this awkward creature into a sleek and modern python.</p>
  553. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/lizard-to-snake.jpg" alt=""/></p>
  554. <p>There are two main reasons to delete features. First, you might discover a feature was a bad idea, through user feedback or your own growing wisdom. That was the case with the quirky behavior of midnight. Or, the feature might have been well-adapted to your library’s environment at first, but the ecology changes. Perhaps another deity invents mammals. Your creature wants to squeeze into their little burrows and eat the tasty mammal filling, so it has to lose its legs.</p>
  555. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/mammal.jpg" alt=""/></p>
  556. <p>Similarly, the Python standard library deletes features in response to changes in the language itself. Consider asyncio’s Lock. It has been awaitable ever since “await” was added as a keyword:</p>
  557. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/>lock <span>=</span> asyncio<span>.</span>Lock()
  558. <span>async</span> <span>def</span> <span>critical_section</span>():
  559. <span> <span>await</span> lock
  560. </span> <span>try</span>:
  561. <span>print</span>(<span>'holding lock'</span>)
  562. <span>finally</span>:
  563. lock<span>.</span>release()
  564. </code></pre></div>
  565. <p>But now, we can do “async with lock”:</p>
  566. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/>lock <span>=</span> asyncio<span>.</span>Lock()
  567. <span>async</span> <span>def</span> <span>critical_section</span>():
  568. <span> <span>async</span> <span>with</span> lock:
  569. </span> <span>print</span>(<span>'holding lock'</span>)
  570. </code></pre></div>
  571. <p>The new style is much better! It’s short, and less prone to mistakes in a big function with other try-except blocks. Since “there should be one and preferably only one obvious way to do it” <a href="https://bugs.python.org/issue32253">the old syntax is deprecated in Python 3.7</a> and it will be banned soon.</p>
  572. <p>It’s inevitable that ecological change will have this effect on your code too, so learn to delete features gently. Before you do so, consider the cost or benefit of deleting it. Responsible maintainers are reluctant to make their users change a large amount of their code, or change their logic. (Remember how painful it was when Python 3 removed the “u” string prefix, before it was added back.) If the code changes are mechanical, however, like a simple search and replace, or if the feature is dangerous, it may be worth deleting.</p>
  573. <div><p>
  574. Whether to Delete a Feature
  575. </p></div>
  576. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/scale.jpg" alt=""/></p>
  577. <table class="pro-con">
  578. <thead>
  579. <tr>
  580. <th>Con</th>
  581. <th>Pro</th>
  582. </tr>
  583. </thead>
  584. <tbody>
  585. <tr>
  586. <td>Code must change</td>
  587. <td>Change is mechanical</td>
  588. </tr>
  589. <tr>
  590. <td>Logic must change</td>
  591. <td>Feature is dangerous</td>
  592. </tr>
  593. </tbody>
  594. </table>
  595. <p>In the case of our hungry lizard, we decide to delete its legs so it slither into a mouse’s hole and eat it. How do we go about this? We could just delete the <code>walk</code> method, changing code from this:</p>
  596. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>class</span> <span>Reptile</span>:
  597. <span>def</span> <span>walk</span>(<span>self</span>):
  598. <span>print</span>(<span>'step step step'</span>)
  599. </code></pre></div>
  600. <p>To this:</p>
  601. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>class</span> <span>Reptile</span>:
  602. <span>def</span> <span>slither</span>(<span>self</span>):
  603. <span>print</span>(<span>'slide slide slide'</span>)
  604. </code></pre></div>
  605. <p>That’s not a good idea, the creature is used to walking! Or, in terms of a library, your users have code that relies on the existing method. When they upgrade to the latest version of your library, their code will break.</p>
  606. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># User's code. Oops!</span>
  607. Reptile<span>.</span>walk()
  608. </code></pre></div>
  609. <p>Therefore responsible creators make this promise:</p>
  610. <div><p>
  611. Fifth Covenant:<br/>Delete Features Gently
  612. </p></div>
  613. <p>There’s a few steps involved in deleting a feature gently. Starting with a lizard that walks with its legs, you first add the new method, “slither”. Next, deprecate the old method.</p>
  614. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span>import</span> <span>warnings</span>
  615. <span>class</span> <span>Reptile</span>:
  616. <span>def</span> <span>walk</span>(<span>self</span>):
  617. warnings<span>.</span>warn(
  618. <span>"walk is deprecated, use slither"</span>,
  619. <span>DeprecationWarning</span>, stacklevel<span>=2</span>)
  620. <span>print</span>(<span>'step step step'</span>)
  621. <span>def</span> <span>slither</span>(<span>self</span>):
  622. <span>print</span>(<span>'slide slide slide'</span>)
  623. </code></pre></div>
  624. <p>The Python warnings module is quite powerful. By default it prints warnings to stderr, only once per code location, but you can silence warnings or turn them into exceptions, among other options.</p>
  625. <p>As soon as you add this warning to your library, PyCharm and other IDEs render the deprecated method with a strikethrough. Users know right away that the method is due for deletion.</p>
  626. <pre><code>Reptile().<span>walk()</span></code></pre>
  627. <p>What happens when they run their code with the upgraded library?</p>
  628. <div class="highlight"><pre><code class="language-text" data-lang="text"><span/>&gt; python3 script.py
  629. DeprecationWarning: walk is deprecated, use slither
  630. script.py:14: Reptile().walk()
  631. step step step
  632. </code></pre></div>
  633. <p>By default, they see a warning on stderr, but the script succeeds and prints “step step step”. The warning’s traceback shows what line of the user’s code must be fixed. (That’s what the “stacklevel” argument does: it shows the call site that users need to change, not the line where the warning is generated.) Notice that the error message is instructive, it describes what a library user must do to migrate to the new version.</p>
  634. <p>Your users will want to test their code and prove they call no deprecated library methods. Warnings alone won’t make unittests fail, but exceptions will. Python has a command-line option to turn deprecation warnings into exceptions:</p>
  635. <div class="highlight"><pre><code class="language-text" data-lang="text"><span/>&gt; python3 -Werror::DeprecationWarning script.py
  636. Traceback (most recent call last):
  637. File "script.py", line 14, in &lt;module&gt;
  638. Reptile().walk()
  639. File "script.py", line 8, in walk
  640. DeprecationWarning, stacklevel=2)
  641. DeprecationWarning: walk is deprecated, use slither
  642. </code></pre></div>
  643. <p>Now, “step step step” is not printed, because the script terminates with an error.</p>
  644. <p>So once you’ve released a version of your library that warns about the deprecated “walk” method, you can delete it safely in the next release. Right?</p>
  645. <p>Consider what your library’s users might have in their projects’ requirements:</p>
  646. <pre><code># User's requirements.txt has a dependency on the reptile package.
  647. reptile
  648. </code></pre>
  649. <p>The next time they deploy their code, they’ll install the latest version of your library. If they haven’t yet handled all deprecations then their code will break, because it still depends on “walk”. You need to be gentler than this. There are three more promises you must keep to your users: to maintain a changelog, choose a version scheme, and write an upgrade guide.</p>
  650. <div><p>
  651. Sixth Covenant:<br/>Maintain a Changelog
  652. </p></div>
  653. <p>Your library must have a change log; its main purpose is to announce when a feature that your users rely on is deprecated or deleted.</p>
  654. <div>
  655. <p>Changes in Version 1.1</p>
  656. <p><strong>New features</strong></p>
  657. <ul><li>New function Reptile.slither()</li></ul>
  658. <p><strong>Deprecations</strong></p>
  659. <ul><li>Reptile.walk() is deprecated and will be removed in version 2.0, use slither()</li></ul></div>
  660. <p>Responsible creators use version numbers to express how a library has changed, so users can make informed decisions about upgrading. A “version scheme” is a language for communicating the pace of change.</p>
  661. <div><p>
  662. Seventh Covenant:<br/>Choose a Version Scheme
  663. </p></div>
  664. <p>There are two schemes in widespread use, <a href="https://semver.org">semantic versioning</a> and time-based versioning. I recommend semantic versioning for nearly any library. The Python flavor thereof is defined in <a href="https://www.python.org/dev/peps/pep-0440/">PEP 440</a>, and tools like “pip” understand semantic version numbers.</p>
  665. <p>If you choose semantic versioning for your library, you can delete its legs gently with version numbers like:</p>
  666. <blockquote>
  667. <p>1.0: First “stable” release, with walk()<br/>
  668. 1.1: Add slither(), deprecate walk()<br/>
  669. 2.0: Delete walk()</p>
  670. </blockquote>
  671. <p>Your users should depend on a range of your library’s versions like so:</p>
  672. <pre><code># User's requirements.txt.
  673. reptile&gt;=1,&lt;2
  674. </code></pre>
  675. <p>This allows them to upgrade automatically within a major release, receiving bugfixes and potentially raising some deprecation warnings, but not upgrading to the <strong>next</strong> major release and risking a change that breaks their code.</p>
  676. <p>If you follow time-based version your releases might be numbered thus:</p>
  677. <blockquote>
  678. <p>2017.06.0: A release in June 2017<br/>
  679. 2018.11.0: Add slither(), deprecate walk()<br/>
  680. 2019.04.0: Delete walk()</p>
  681. </blockquote>
  682. <p>And users can depend on your library like:</p>
  683. <pre><code># User's requirements.txt for time-based version.
  684. reptile==2018.11.*
  685. </code></pre>
  686. <p>This is terrific, but how do your users know your versioning scheme and how to test their code for deprecations? You have to advise them how to upgrade.</p>
  687. <div><p>
  688. Eighth Covenant:<br/>Write an Upgrade Guide
  689. </p></div>
  690. <p>Here’s how a responsible library creator might guide users:</p>
  691. <div>
  692. <p>Upgrading to 2.0</p>
  693. <p><strong>Migrate from Deprecated APIs</strong></p>
  694. <p>See the <span>changelog</span> for deprecated features.</p>
  695. <p><strong>Enable Deprecation Warnings</strong></p>
  696. <p>Upgrade to 1.1 and test your code with:</p>
  697. <code>python -Werror::DeprecationWarning</code>
  698. <p>Now it's safe to upgrade.</p></div>
  699. <p>You must teach users how to handle deprecation warnings by showing them the command line options. Not all Python programmers know this—I certainly have to look up the syntax each time. And take note, you must <strong>release</strong> a version that prints warnings from each deprecated API, so users can test with that version before upgrading again. In this example, version 1.1 is the bridge release. It allows your users to rewrite their code incrementally, fixing each deprecation warning separately until they have entirely migrated to the latest API. They can test changes to their code, and changes in your library, independently from each other, and isolate the cause of bugs.</p>
  700. <p>If you chose semantic versioning, this transitional period lasts until the next major release, from 1.x to 2.0, or from 2.x to 3.0, and so on. The gentle way to delete a creature’s legs is to give it at least one version in which to adjust its lifestyle. Don’t remove the legs all at once!</p>
  701. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/skink.jpg" alt=""/></p>
  702. <p>Version numbers, deprecation warnings, the changelog, and the upgrade guide work together to gently evolve your library without breaking the covenant with your users. The <a href="https://twistedmatrix.com/documents/current/core/development/policy/compatibility-policy.html">Twisted project’s Compatibility Policy</a> explains this beautifully:</p>
  703. <blockquote>
  704. <p>“The First One’s Always Free”</p>
  705. <p>Any application which runs without warnings may be upgraded one minor version of Twisted.</p>
  706. <p>In other words, any application which runs its tests without triggering any warnings from Twisted should be able to have its Twisted version upgraded at least once with no ill effects except the possible production of new warnings.</p>
  707. </blockquote>
  708. <p>Now, we creator deities have gained the wisdom and power to add features by adding methods, and to delete them gently. We can also add features by adding parameters, but this brings a new level of difficulty. Are you ready?</p>
  709. <h1 id="adding-parameters">Adding Parameters</h1>
  710. <p>Imagine that you just gave your snake-like creature a pair of wings. Now you must allow it the choice whether to move by slithering or flying. Currently its “move” function takes one parameter:</p>
  711. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  712. <span>def</span> <span>move</span>(direction):
  713. <span>print</span>(f<span>'slither </span><span>{direction}</span><span>'</span>)
  714. <span># A user's application.</span>
  715. move(<span>'north'</span>)
  716. </code></pre></div>
  717. <p>You want to add a “mode” parameter, but this breaks your users’ code if they upgrade, because they pass only one argument:</p>
  718. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  719. <span>def</span> <span>move</span>(direction, mode):
  720. <span>assert</span> mode <span>in</span> (<span>'slither'</span>, <span>'fly'</span>)
  721. <span>print</span>(f<span>'</span><span>{mode}</span><span> </span><span>{direction}</span><span>'</span>)
  722. <span># A user's application. Error!</span>
  723. <span>move(<span>'north'</span>)
  724. </span></code></pre></div>
  725. <p>A truly wise creator promises not to break users’ code this way.</p>
  726. <div><p>
  727. Ninth Covenant:<br/>Add Parameters Compatibly
  728. </p></div>
  729. <p>To keep this covenant, add each new parameter with a default value that preserves the original behavior.</p>
  730. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  731. <span>def</span> <span>move</span>(direction, mode<span>=</span><span>'slither'</span>):
  732. <span>assert</span> mode <span>in</span> (<span>'slither'</span>, <span>'fly'</span>)
  733. <span>print</span>(f<span>'</span><span>{mode}</span><span> </span><span>{direction}</span><span>'</span>)
  734. <span># A user's application.</span>
  735. move(<span>'north'</span>)
  736. </code></pre></div>
  737. <p>Over time, parameters are the natural history of your function’s evolution. They’re listed oldest first, each with a default value. Library users can pass keyword arguments to opt in to specific new behaviors, and accept the defaults for all others.</p>
  738. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  739. <span>def</span> <span>move</span>(direction,
  740. mode<span>=</span><span>'slither'</span>,
  741. turbo<span>=</span><span>False</span>,
  742. extra_sinuous<span>=</span><span>False</span>,
  743. hail_lyft<span>=</span><span>False</span>):
  744. <span># ...</span>
  745. <span># A user's application.</span>
  746. move(<span>'north'</span>, extra_sinuous<span>=</span><span>True</span>)
  747. </code></pre></div>
  748. <p>There is a danger, however, that a user might write code like this:</p>
  749. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># A user's application, poorly-written.</span>
  750. move(<span>'north'</span>, <span>'slither'</span>, <span>False</span>, <span>True</span>)
  751. </code></pre></div>
  752. <p>What happens if, in the next major version of your library, you get rid of one of the parameters, like “turbo”?</p>
  753. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code, next major version. "turbo" is deleted.</span>
  754. <span>def</span> <span>move</span>(direction,
  755. mode<span>=</span><span>'slither'</span>,
  756. extra_sinuous<span>=</span><span>False</span>,
  757. hail_lyft<span>=</span><span>False</span>):
  758. <span># ...</span>
  759. <span># A user's application, poorly-written.</span>
  760. move(<span>'north'</span>, <span>'slither'</span>, <span>False</span>, <span>True</span>)
  761. </code></pre></div>
  762. <p>The user’s code still compiles, and this is a bad thing. The code stopped moving extra-sinuously and started hailing a Lyft, which was not the intention. I trust that you can predict what I’ll say next: deleting a parameter requires several steps. First, of course, deprecate the “turbo” parameter. I like a technique like this one that detects whether any user’s code is relying on this parameter:</p>
  763. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  764. _turbo_default <span>=</span> <span>object</span>()
  765. <span>def</span> <span>move</span>(direction,
  766. mode<span>=</span><span>'slither'</span>,
  767. turbo<span>=</span>_turbo_default,
  768. extra_sinuous<span>=</span><span>False</span>,
  769. hail_lyft<span>=</span><span>False</span>):
  770. <span>if</span> turbo <span>is</span> <span>not</span> _turbo_default:
  771. warnings<span>.</span>warn(
  772. <span>"'turbo' is deprecated"</span>,
  773. <span>DeprecationWarning</span>,
  774. stacklevel<span>=2</span>)
  775. <span>else</span>:
  776. <span># The old default.</span>
  777. turbo <span>=</span> <span>False</span>
  778. </code></pre></div>
  779. <p>But your users might not notice the warning. Warnings are not very loud: they can be suppressed, or lost in log files. Users might heedlessly upgrade to the next major version of your library, the version that deletes “turbo”. Their code will run without error and silently do the wrong thing! As the Zen of Python says, “Errors should never pass silently.” Indeed, reptiles hear poorly, so you must correct them very loudly when they make mistakes.</p>
  780. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/loudly.jpg" alt=""/></p>
  781. <p>The best way to protect your users is with Python 3’s star syntax, which requires callers to pass keyword arguments.</p>
  782. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code.</span>
  783. <span># All arguments after "*" must be passed by keyword.</span>
  784. <span>def</span> <span>move</span>(direction,
  785. <span>*</span>,
  786. mode<span>=</span><span>'slither'</span>,
  787. turbo<span>=</span><span>False</span>,
  788. extra_sinuous<span>=</span><span>False</span>,
  789. hail_lyft<span>=</span><span>False</span>):
  790. <span># ...</span>
  791. <span># A user's application, poorly-written.</span>
  792. <span># Error! Can't use positional args, keyword args required.</span>
  793. move(<span>'north'</span>, <span>'slither'</span>, <span>False</span>, <span>True</span>)
  794. </code></pre></div>
  795. <p>With the star in place, this is the only syntax allowed:</p>
  796. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># A user's application.</span>
  797. move(<span>'north'</span>, extra_sinuous<span>=</span><span>True</span>)
  798. </code></pre></div>
  799. <p>Now when you delete “turbo”, you can be certain any user code that relies on it will fail loudly. If your library also supports Python 2, there’s no shame in that, you can simulate the star syntax thus (<a href="http://www.informit.com/articles/article.aspx?p=2314818">credit to Brett Slatkin</a>):</p>
  800. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># Your library code, Python 2 compatible.</span>
  801. <span>def</span> <span>move</span>(direction, <span>**</span>kwargs):
  802. mode <span>=</span> kwargs<span>.</span>pop(<span>'mode'</span>, <span>'slither'</span>)
  803. sinuous <span>=</span> kwargs<span>.</span>pop(<span>'extra_sinuous'</span>, <span>False</span>)
  804. lyft <span>=</span> kwargs<span>.</span>pop(<span>'hail_lyft'</span>, <span>False</span>)
  805. <span>if</span> kwargs:
  806. <span>raise</span> <span>TypeError</span>(<span>'Unexpected kwargs: </span><span>%r</span><span>'</span>
  807. <span>%</span> kwargs)
  808. <span># ...</span>
  809. </code></pre></div>
  810. <p>Requiring keyword arguments is a wise choice, but it requires foresight. If you allow an argument to be passed positionally, you cannot convert it to keyword-only in a later release. So, add the star now. You can observe in the asyncio API that it uses the star pervasively in constructors, methods, and functions. Even though “Lock” only takes one optional parameter so far, the asyncio developers added the star right away. This is providential.</p>
  811. <div class="highlight"><pre><code class="language-py3" data-lang="py3"><span/><span># In asyncio.</span>
  812. <span>class</span> <span>Lock</span>:
  813. <span>def</span> <span>__init__</span>(<span>self</span>, <span>*</span>, loop<span>=</span><span>None</span>):
  814. <span># ...</span>
  815. </code></pre></div>
  816. <p>Now we’ve gained the wisdom to change methods and parameters while keeping our covenant with users. The time has come to try the most challenging kind of evolution: changing behavior without changing either methods or parameters.</p>
  817. <h1 id="changing-behavior">Changing Behavior</h1>
  818. <p>Let’s say your creature is a rattlesnake, and you want to teach it a new behavior.</p>
  819. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/rattlesnake.jpg" alt=""/></p>
  820. <p>Sidewinding! The creature’s body will appear the same, but its behavior will change. How can we prepare it for this step of its evolution?</p>
  821. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/sidewinding.jpg" alt=""/></p>
  822. <p>A responsible creator can learn from the following example in the Python standard library, when behavior changed without a new function or parameters. Once upon a time, the os.stat function was introduced to get file statistics, like the creation time. At first, times were always integers.</p>
  823. <div class="highlight"><pre><code class="language-pycon" data-lang="pycon"><span/><span>&gt;&gt;&gt; </span>os<span>.</span>stat(<span>'file.txt'</span>)<span>.</span>st_ctime
  824. <span>1540817862</span>
  825. </code></pre></div>
  826. <p>One day, the core developers decided to use floats for os.stat times, to give sub-second precision. But they worried that existing user code wasn’t ready for the change.
  827. They created a setting in Python 2.3, “stat_float_times”, that was false by default. A user could set it to True to opt in to floating-point timestamps.</p>
  828. <div class="highlight"><pre><code class="language-pycon" data-lang="pycon"><span/><span>&gt;&gt;&gt; </span><span># Python 2.3.</span>
  829. <span>&gt;&gt;&gt; </span>os<span>.</span>stat_float_times(<span>True</span>)
  830. <span>&gt;&gt;&gt; </span>os<span>.</span>stat(<span>'file.txt'</span>)<span>.</span>st_ctime
  831. <span>1540817862.598021</span>
  832. </code></pre></div>
  833. <p>Starting in Python 2.5, float times became the default, so any new code written for 2.5 and later could ignore the setting and expect floats. Of course, you could set it to False to keep the old behavior, or set it to True to ensure the new behavior in all Python versions, and prepare your code for the day when stat_float_times is deleted.</p>
  834. <p>Ages passed. In Python 3.1 the setting was deprecated to prepare people for the distant future, and finally, after its decades-long journey, <a href="https://bugs.python.org/issue31827">the setting was removed</a>. Float times are now the only option. It’s a long road, but responsible deities are patient because we know this gradual process has a good chance of saving users from unexpected behavior changes.</p>
  835. <div><p>
  836. Tenth Covenant:<br/>Change Behavior Gradually
  837. </p></div>
  838. <p>Here are the steps:</p>
  839. <ul>
  840. <li>Add a flag to opt in to the new behavior, default False, warn if it’s False<br/></li>
  841. <li>Change default to True, deprecate flag entirely<br/></li>
  842. <li>Remove the flag</li>
  843. </ul>
  844. <p>If you follow semantic versioning, the versions might be like so:</p>
  845. <table class="versions-table" rules="rows">
  846. <thead>
  847. <tr>
  848. <th>Library version</th><th>Library API</th><th>User code</th>
  849. </tr>
  850. </thead>
  851. <tbody>
  852. <tr>
  853. </tr><tr><td>1.0</td><td>No flag</td><td>Expect old behavior</td>
  854. </tr><tr><td>1.1</td><td>Add flag, default False,<br/>
  855. warn if it's False </td><td>Set flag True,<br/>
  856. handle new behavior</td></tr>
  857. <tr><td>2.0</td><td>Change default to True,<br/>
  858. deprecate flag entirely</td><td>Handle new behavior</td></tr>
  859. <tr><td>3.0</td><td>Remove flag</td><td>Handle new behavior</td></tr>
  860. </tbody></table>
  861. <p>You need <strong>two</strong> major releases to complete the maneuver. If you had gone straight from “Add flag, default False, warn if it’s False” to “Remove flag” without the intervening release, your users’ code would be unable to upgrade. User code written correctly for 1.1, which sets the flag to True and handles the new behavior, must be able to upgrade to the next release with no ill effect except new warnings, but if the flag were deleted in the next release that code would break. A responsible deity never violates the Twisted policy: “The First One’s Always Free.”</p>
  862. <h1 id="the-responsible-creator">The Responsible Creator</h1>
  863. <p><img src="https://emptysqua.re/blog/api-evolution-the-right-way/demeter.jpg" alt=""/></p>
  864. <p>Our ten covenants belong loosely in three categories:</p>
  865. <h3 id="evolve-cautiously">Evolve Cautiously</h3>
  866. <ol>
  867. <li>Avoid Bad Features</li>
  868. <li>Minimize Features</li>
  869. <li>Keep Features Narrow</li>
  870. <li>Mark Experimental Features “Provisional”</li>
  871. <li>Delete Features Gently</li>
  872. </ol>
  873. <h3 id="record-history-rigorously">Record History Rigorously</h3>
  874. <ol>
  875. <li>Maintain a Changelog</li>
  876. <li>Choose a Version Scheme</li>
  877. <li>Write an Upgrade Guide</li>
  878. </ol>
  879. <h3 id="change-slowly-and-loudly">Change Slowly and Loudly</h3>
  880. <ol>
  881. <li>Add Parameters Compatibly</li>
  882. <li>Change Behavior Gradually</li>
  883. </ol>
  884. <p>If you keep these covenants with your creature, you’ll be a responsible creator deity. Your creature’s body can evolve over time, forever improving and adapting to changes in its environment, but without sudden changes the creature isn’t prepared for. If you maintain a library, keep these promises to your users, and you can innovate your library without breaking the code of the people who rely on you.</p>
  885. <hr/>
  886. <p>Illustrations:</p>
  887. <ul>
  888. <li><a href="https://www.gutenberg.org/files/42224/42224-h/42224-h.htm">The World’s Progress, The Delphian Society, 1913</a></li>
  889. <li><a href="https://publicdomainreview.org/product-att/artist/charles-owen/">Essay Towards a Natural History of Serpents, Charles Owen, 1742</a></li>
  890. <li><a href="https://archive.org/details/onbatrachiarepti00cope/page/n3">On the batrachia and reptilia of Costa Rica: With notes on the herpetology and ichthyology of Nicaragua and Peru, Edward Drinker Cope, 1875</a></li>
  891. <li><a href="https://www.flickr.com/photos/internetarchivebookimages/20556001490">Natural History, Richard Lydekker et. al., 1897</a></li>
  892. <li><a href="https://www.oldbookillustrations.com/illustrations/stationery/">Mes Prisons, Silvio Pellico, 1843</a></li>
  893. <li><a href="https://www.alamy.com/mediacomp/ImageDetails.aspx?ref=D7Y61W">Tierfotoagentur / m.blue-shadow</a></li>
  894. <li><a href="https://www.vintag.es/2013/06/riding-alligator-c-1930s.html">From Los Angeles Public Library, 1930</a></li>
  895. </ul>
  896. </div>
  897. </article>
  898. </section>
  899. <nav id="jumpto">
  900. <p>
  901. <a href="/david/blog/">Accueil du blog</a> |
  902. <a href="https://emptysqua.re/blog/api-evolution-the-right-way/">Source originale</a> |
  903. <a href="/david/stream/2019/">Accueil du flux</a>
  904. </p>
  905. </nav>
  906. <footer>
  907. <div>
  908. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  909. <p>
  910. Bonjour/Hi!
  911. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  912. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  913. </p>
  914. <p>
  915. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  916. </p>
  917. <p>
  918. Voici quelques articles choisis :
  919. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  920. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  921. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  922. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  923. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  924. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  925. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  926. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  927. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  928. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  929. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  930. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  931. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  932. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  933. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  934. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  935. </p>
  936. <p>
  937. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  938. </p>
  939. <p>
  940. Je ne traque pas ta navigation mais mon
  941. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  942. conserve des logs d’accès.
  943. </p>
  944. </div>
  945. </footer>
  946. <script type="text/javascript">
  947. ;(_ => {
  948. const jumper = document.getElementById('jumper')
  949. jumper.addEventListener('click', e => {
  950. e.preventDefault()
  951. const anchor = e.target.getAttribute('href')
  952. const targetEl = document.getElementById(anchor.substring(1))
  953. targetEl.scrollIntoView({behavior: 'smooth'})
  954. })
  955. })()
  956. </script>