{
    "archive_path": "archive/1765881132.523814",
    "base_url": "blog.glyph.im/2025/04/stop-writing-init-methods.html",
    "basename": "stop-writing-init-methods.html",
    "bookmarked_date": "2025-12-16 10:32",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/blog.glyph.im/2025/04/stop-writing-init-methods.html",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=blog.glyph.im",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": null
    },
    "domain": "blog.glyph.im",
    "downloaded_at": "2025-12-16T10:32:19.013946+00:00",
    "downloaded_datestr": "2025-12-16 10:32",
    "extension": "html",
    "hash": "X6Z5R80AVTXRV33VE467",
    "history": {
        "archive_org": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://web.archive.org/save/https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2025-12-16T10:34:14.593591+00:00",
                "index_texts": null,
                "output": "TimeoutExpired: Command '['/usr/bin/curl', '--silent', '--location', '--compressed', '--proxy', 'socks5://tor-socks-proxy:9150', '--head', '--max-time', '60', '--user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)', 'https://web.archive.org/save/https://blog.glyph.im/2025/04/stop-writing-init-methods.html']' timed out after 60 seconds",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:33:14.483627+00:00",
                "status": "failed"
            }
        ],
        "dom": [
            {
                "cmd": [
                    "/usr/bin/chromium-browser",
                    "--proxy-server=socks5://tor-socks-proxy:9150",
                    "--disable-features=DarkMode",
                    "--run-all-compositor-stages-before-draw",
                    "--hide-scrollbars",
                    "--autoplay-policy=no-user-gesture-required",
                    "--no-first-run",
                    "--use-fake-ui-for-media-stream",
                    "--use-fake-device-for-media-stream",
                    "--simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT'",
                    "--headless=new",
                    "--no-sandbox",
                    "--no-zygote",
                    "--disable-dev-shm-usage",
                    "--disable-software-rasterizer",
                    "--disable-sync",
                    "--window-size=1440,2000",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "--user-data-dir=/data/personas/Default/chrome_profile",
                    "--profile-directory=Default",
                    "--dump-dom",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "131.0.6778",
                "end_ts": "2025-12-16T10:32:35.047484+00:00",
                "index_texts": null,
                "output": "output.html",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:28.172909+00:00",
                "status": "succeeded"
            }
        ],
        "favicon": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://www.google.com/s2/favicons?domain=blog.glyph.im"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2025-12-16T10:32:22.883530+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:19.224941+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2025-12-16T10:32:23.036260+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:22.925197+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [
            {
                "cmd": [
                    "(internal) archivebox.extractors.htmltotext",
                    "./{singlefile,dom}.html"
                ],
                "cmd_version": "0.8.5rc51",
                "end_ts": "2025-12-16T10:32:54.295074+00:00",
                "index_texts": [
                    "(https://login.launchpad.net/+openid) (https://login.launchpad.net/+id/GLCYheW) (https://login.launchpad.net/+openid) (https://login.launchpad.net/+id/GLCYheW) (https://blog.glyph.im/images/favicon.ico) (https://blog.glyph.im/feeds/all.atom.xml) (Deciphering Glyph Atom Feed) Deciphering Glyph ::\n        Stop Writing `__init__` Methods  (https://blog.glyph.im/theme/css/main.css?9046f797)  (https://blog.glyph.im/) Deciphering   Glyph    (https://blog.glyph.im/pages/about.html) About  (https://blog.glyph.im/archives.html) Archives  (https://mastodon.social/@glyph) Mastodon  (https://github.com/glyph) GitHub  (https://blog.glyph.im/pages/patrons.html) Patrons     (https://blog.glyph.im/2025/04/stop-writing-init-methods.html) (Permalink to Stop Writing `__init__` Methods) Stop Writing `__init__` Methods  YEARS OF DATACLASSES yet NO REAL-WORLD USE FOUND for overriding\nspecial methods just so you can have some attributes.  (/tag/python.html) python (/tag/programming.html) programming  (https://blog.glyph.im/2025/04/stop-writing-init-methods.html) (Permalink to Stop Writing `__init__` Methods) Thursday April 17, 2025     The History Before dataclasses were added to Python in version 3.7 \u2014 in June of 2018 \u2014 the __init__ special method had an important use.  If you had a class\nrepresenting a data structure \u2014 for example a 2DCoordinate , with x and y attributes \u2014 you would want to be able to construct it as 2DCoordinate(x=1,\ny=2) , which would require you to add an __init__ method with x and y parameters. The other options available at the time all had pretty bad problems: You could remove 2DCoordinate from your public API and instead expose a make_2d_coordinate function and make it non-importable, but then how would\n   you document your return or parameter types? You could document the x and y attributes and make the user assign each\n   one themselves, but then 2DCoordinate() would return an invalid object. You could default your coordinates to 0 with class attributes, and while\n   that would fix the problem with option 2, this would now require all 2DCoordinate objects to be not just mutable, but mutated at every call\n   site. You could fix the problems with option 1 by adding a new abstract class\n   that you could expose in your public API, but this would explode the\n   complexity of every new public class, no matter how simple.  To make matters\n   worse, typing.Protocol didn\u2019t even arrive until Python 3.8, so, in the\n   pre-3.7 world this would condemn you to using concrete inheritance and\n   declaring multiple classes even for the most basic data structure\n   imaginable.  Also, an __init__ method that does nothing but assign a few attributes doesn\u2019t have any significant problems, so it is an obvious choice in this case.\nGiven all the problems that I just described with the alternatives, it makes\nsense that it became the obvious default choice, in most cases. However, by accepting \u201cdefine a custom __init__ \u201d as the default way to\nallow users to create your objects, we make a habit of beginning every class\nwith a pile of arbitrary code that gets executed every time it is\ninstantiated. Wherever there is arbitrary code, there are arbitrary problems. The Problems Let\u2019s consider a data structure more complex than one that simply holds a\ncouple of attributes.  We will create one that represents a reference to some\nI/O in the external world: a FileReader . Of course Python has (https://docs.python.org/3.13/library/io.html#io.FileIO) its own open-file object\nabstraction , but I\nwill be ignoring that for the purposes of the example. Let\u2019s assume a world where we have the following functions, in an imaginary fileio module: open(path: str) -> int  read(fileno: int, length: int)  close(fileno: int)   Our hypothetical fileio.open returns an integer representing a file\ndescriptor1  , fileio.read allows us to read length bytes from an open\nfile descriptor, and fileio.close closes that file descriptor, invalidating\nit for future use. With the habit that we have built from writing thousands of __init__ methods,\nwe might want to write our FileReader class like this: 1 2 3 4 5 6 7     class FileReader : def __init__ ( self , path : str ) -> None : self . _fd = fileio . open ( path ) def read ( self , length : int ) -> bytes : return fileio . read ( self . _fd , length ) def close ( self ) -> None : fileio . close ( self . _fd )         For our initial use-case, this is fine.  Client code creates a FileReader by\ndoing something like FileReader(\"./config.json\") , which always creates a FileReader that maintains its file descriptor int internally as private\nstate.  This is as it should be; we don\u2019t want user code to see or mess with _fd , as that might violate FileReader \u2019s invariants.  All the necessary work\nto construct a valid FileReader \u2014 i.e. the call to open \u2014 is always taken\ncare of for you by FileReader.__init__ . However, additional requirements will creep in, and as they do, FileReader.__init__ becomes increasingly awkward. Initially we only care about fileio.open , but later, we may have to deal with\na library that has its own reasons for managing the call to fileio.open by\nitself, and wants to give us an int that we use as our _fd , we now have to\nresort to weird workarounds like: 1 2 3 4     def reader_from_fd ( fd : int ) -> FileReader : fr = object . __new__ ( FileReader ) fr . _fd = fd return fr         Now, all those nice properties that we got from trying to force object\nconstruction to give us a valid object are gone. reader_from_fd \u2019s type\nsignature, which takes a plain int , has no way of even suggesting to client\ncode how to ensure that it has passed in the right kind of int . Testing is much more of a hassle, because we have to patch in our own copy of fileio.open any time we want an instance of a FileReader in a test without\ndoing any real-life file I/O, even if we could (for example) share a single\nfile descriptor among many FileReader s for testing purposes. All of this also assumes a fileio.open that is synchronous .  Although for\nliteral file I/O this is more of a (https://stackoverflow.com/questions/87892/what-is-the-status-of-posix-asynchronous-i-o-aio) hypothetical concern, there are many types of networked resource which are really only\navailable via an asynchronous (and thus: potentially slow, potentially\nerror-prone) API.  If you\u2019ve ever found yourself wanting to type async def\n__init__(self): ... then you have seen this limitation in practice. Comprehensively describing all the possible problems with this approach would\nend up being a book-length treatise on a philosophy of object oriented design,\nso I will sum up by saying that the cause of all these problems is the same:\nwe are inextricably linking the act of creating a data structure with whatever side-effects are  most often associated with that data structure.\nIf they are \u201coften\u201d associated with it, then by definition they are not\n\u201calways\u201d associated with it, and all the cases where they aren\u2019t associated\nbecome unweildy and potentially broken. Defining an __init__ is an anti-pattern, and we need a replacement for it. The Solutions I believe this tripartite assemblage of design techniques will address the\nproblems raised above: using dataclass to define attributes, replacing behavior that previously would have previously been in __init__ with a new classmethod that does the same thing, and using precise types to describe what a valid instance looks like.  Using dataclass attributes to create an __init__ for you To begin, let\u2019s refactor FileReader into a dataclass .  This does get us an __init__ method, but it won\u2019t be one an arbitrary one we define ourselves;\nit will get the useful constraint enforced on it that it will just assign\nattributes. 1 2 3 4 5 6 7     @dataclass class FileReader : _fd : int def read ( self , length : int ) -> bytes : return fileio . read ( self . _fd , length ) def close ( self ) -> None : fileio . close ( self . _fd )         Except... oops.  In fixing the problems that we created with our custom __init__ that calls fileio.open , we have re-introduced several problems\nthat it solved: We have removed all the convenience of FileReader(\"path\") .  Now the user\n   needs to import the low-level fileio.open again, making the most common\n   type of construction both more verbose and less discoverable; if we want\n   users to know how to build a FileReader in a practical scenario, we will\n   have to add something in our documentation to point at a separate module\n   entirely. There\u2019s no enforcement of the validity of _fd as a file descriptor; it\u2019s\n   just some integer, which the user could easily pass an incorrect instance\n   of, with no error.  In isolation, dataclass by itself can\u2019t solve all our problems, so let\u2019s add\nin the second technique. Using classmethod factories to create objects We don\u2019t want to require any additional imports, or require users to go looking\nat any other modules \u2014 or indeed anything other than FileReader itself \u2014 to\nfigure out how to create a FileReader for its intended usage. Luckily we have a tool that can easily address all of these concerns at once: @classmethod .  Let\u2019s define a FileReader.open class method: 1 2 3 4 5 6 7     from typing import Self @dataclass class FileReader : _fd : int @classmethod def open ( cls , path : str ) -> Self : return cls ( fileio . open ( path ))         Now, your callers can replace FileReader(\"path\") with FileReader.open(\"path\") , and get all the same benefits. Additionally, if we needed to await fileio.open(...) , and thus we needed its\nsignature to be @classmethod async def open , we are freed from the constraint\nof __init__ as a special method.  There is nothing that would prevent a @classmethod from being async , or indeed, from having any other\nmodification to its return value, such as returning a tuple of related values\nrather than just the object being constructed. Using NewType to address object validity Next, let\u2019s address the slightly trickier issue of enforcing object validity. Our type signature calls this thing an int , and indeed, that is unfortunately\nwhat the lower-level fileio.open gives us, and that\u2019s beyond our control.\nBut for our own purposes, we can be more precise in our definitions, using (https://docs.python.org/3.13/library/typing.html#newtype) NewType  : 1 2     from typing import NewType FileDescriptor = NewType ( \"FileDescriptor\" , int )         There are a few different ways to address the underlying library, but for the\nsake of brevity and to illustrate that this can be done with zero run-time\noverhead, let\u2019s just insist to Mypy that we have versions of fileio.open , fileio.read , and fileio.write which actually already take FileDescriptor integers rather than regular ones. 1 2 3 4     from typing import Callable _open : Callable [[ str ], FileDescriptor ] = fileio . open # type:ignore[assignment] _read : Callable [[ FileDescriptor , int ], bytes ] = fileio . read _close : Callable [[ FileDescriptor ], None ] = fileio . close         We do of course have to slightly adjust FileReader , too, but the changes are\nvery small.  Putting it all together, we get: 1 2 3 4 5 6 7 8 9 10 11     from typing import Self @dataclass class FileReader : _fd : FileDescriptor @classmethod def open ( cls , path : str ) -> Self : return cls ( _open ( path )) def read ( self , length : int ) -> bytes : return _read ( self . _fd , length ) def close ( self ) -> None : _close ( self . _fd )         Note that the main technique here is not necessarily using NewType specifically, but rather aligning an instance\u2019s property of \u201chas all attributes\nset\u201d as closely as possible with an instance\u2019s property of \u201cfully valid\ninstance of its class\u201d; NewType is just a handy tool to enforce any necessary\nconstraints on the places where you need to use a primitive type like int , str or bytes . In Summary - The New Best Practice From now on, when you\u2019re defining a new Python class: Make it a dataclass2  . Use its default __init__ method3  . Add @classmethod s to provide your users convenient and discoverable ways to\n  build your objects. Require that all dependencies be satisfied by attributes, so you always\n  start with a valid object. Use typing.NewType to enforce any constraints on primitive data types (like int and str ) which might have magical external attributes, like needing\n  to come from a particular library, needing to be random, and so on.  If you define all your classes this way, you will get all the benefits of a\ncustom __init__ method: All consumers of your data structures will receive valid objects, because an\n  object with all its attributes populated correctly is inherently valid. Users of your library will be presented with convenient ways to create your\n  objects that do as much work as is necessary to make them easy to use, and\n  they can discover these just by looking at the methods on your class itself.  Along with some nice new benefits: You will be future-proofed against new requirements for different ways that\n    users may need to construct your object. If there are already multiple ways to instantiate your class, you can now\n    give each of them a meaningful name; no need to have monstrosities like def __init__(self, maybe_a_filename: int | str | None = None):  Your test suite can always construct an object by satisfying all its\n    dependencies; no need to monkey-patch anything when you can always call the\n    type and never do any I/O or generate any side effects.  Before dataclasses, it was always a bit weird that such a basic feature of the\nPython language \u2014 giving data to a data structure to make it valid \u2014 required\noverriding a method with 4 underscores in its name. __init__ stuck out like\na sore thumb.  Other such methods like __add__ or even __repr__ were\ninherently customizing esoteric attributes of classes. For many years now, that historical language wart has been\nresolved. @dataclass , @classmethod , and NewType give you everything you\nneed to build classes which are convenient, idiomatic, flexible, testable, and\nrobust. Acknowledgments Thank you to (/pages/patrons.html) my patrons who are supporting my writing on\nthis blog.  If you like what you\u2019ve read here and you\u2019d like to read more of\nit, or you\u2019d like to support my (https://github.com/glyph/) various open-source\nendeavors , you can (/pages/patrons.html) support my work as a\nsponsor !  I am also (mailto:consulting@glyph.im) available for\nconsulting work if you think your organization\ncould benefit from expertise on topics like \u201cbut what is a \u2018class\u2019, really?\u201d. If you aren\u2019t already familiar, a \u201cfile descriptor\u201d is an integer which\nhas meaning only within your program; you tell the operating system to open\na file, it says \u201cI have opened file 7 for you\u201d, and then whenever you refer\nto \u201c7\u201d it is that file, until you close(7) . (Jump back to footnote 1 in the text) \u21a9   Or an (https://blog.glyph.im/2016/08/attrs.html) attrs class , if you\u2019re nasty. (Jump back to footnote 2 in the text) \u21a9   Unless you have a really good reason to, of course.  Backwards\ncompatibility, or compatibility with another library, might be good reasons\nto do that.  Or certain types of data-consistency validation which cannot\nbe expressed within the type system.  The most common example of these\nwould be a class that requires consistency between two different fields,\nsuch as a \u201crange\u201d object where start must always be less than end .\nThere are always exceptions to these types of rules.  Still, it\u2019s pretty\nmuch never a good idea to do any I/O in __init__ , and nearly all of the\nremaining stuff that may sometimes be a good idea in edge-cases can be\nachieved with a (https://docs.python.org/3.13/library/dataclasses.html#dataclasses.__post_init__) __post_init__  rather than writing a literal __init__ . (Jump back to footnote 3 in the text) \u21a9         \u00a9 Glyph 2025; All Rights Reserved Excepting Those Which Are Not.  See (https://blog.glyph.im/pages/disclosures.html) my disclosure statements for information on my interests, financial and otherwise.   (https://mastodon.social/@glyph) Mastodon   "
                ],
                "output": "htmltotext.txt",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:54.207927+00:00",
                "status": "succeeded"
            }
        ],
        "media": [
            {
                "cmd": [
                    "/usr/local/bin/yt-dlp",
                    "--restrict-filenames",
                    "--trim-filenames",
                    "128",
                    "--write-description",
                    "--write-info-json",
                    "--write-annotations",
                    "--write-thumbnail",
                    "--no-call-home",
                    "--write-sub",
                    "--write-auto-subs",
                    "--convert-subs=srt",
                    "--yes-playlist",
                    "--continue",
                    "--no-abort-on-error",
                    "--ignore-errors",
                    "--geo-bypass",
                    "--add-metadata",
                    "--format=(bv*+ba/b)[filesize<=750m][filesize_approx<=?750m]/(bv*+ba/b)",
                    "--skip-download",
                    "--cache-dir=/data/yt-dlp-cache/",
                    "--cookies=/data/yt-dlp-cache/cookies.txt",
                    "--proxy=socks5://tor-socks-proxy:9150",
                    "--no-playlist",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "2024.10.7",
                "end_ts": "2025-12-16T10:33:13.451707+00:00",
                "index_texts": [],
                "output": "media/",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:58.168965+00:00",
                "status": "succeeded"
            }
        ],
        "mercury": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/postlight-parser",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "2.2.3",
                "end_ts": "2025-12-16T10:32:54.130124+00:00",
                "index_texts": null,
                "output": "mercury/",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:47.467818+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/readability-extractor",
                    "/tmp/tmpgauci1dr",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2025-12-16T10:32:39.398533+00:00",
                "index_texts": [
                    "The History\nBefore dataclasses were added to Python in version 3.7 \u2014 in June of 2018 \u2014 the\n__init__ special method had an important use.  If you had a class\nrepresenting a data structure \u2014 for example a 2DCoordinate, with x and y\nattributes \u2014 you would want to be able to construct it as 2DCoordinate(x=1,\ny=2), which would require you to add an __init__ method with x and y\nparameters.\nThe other options available at the time all had pretty bad problems:\n\nYou could remove 2DCoordinate from your public API and instead expose a\n   make_2d_coordinate function and make it non-importable, but then how would\n   you document your return or parameter types?\nYou could document the x and y attributes and make the user assign each\n   one themselves, but then 2DCoordinate() would return an invalid object.\nYou could default your coordinates to 0 with class attributes, and while\n   that would fix the problem with option 2, this would now require all\n   2DCoordinate objects to be not just mutable, but mutated at every call\n   site.\nYou could fix the problems with option 1 by adding a new abstract class\n   that you could expose in your public API, but this would explode the\n   complexity of every new public class, no matter how simple.  To make matters\n   worse, typing.Protocol didn\u2019t even arrive until Python 3.8, so, in the\n   pre-3.7 world this would condemn you to using concrete inheritance and\n   declaring multiple classes even for the most basic data structure\n   imaginable.\n\nAlso, an __init__ method that does nothing but assign a few attributes\ndoesn\u2019t have any significant problems, so it is an obvious choice in this case.\nGiven all the problems that I just described with the alternatives, it makes\nsense that it became the obvious default choice, in most cases.\nHowever, by accepting \u201cdefine a custom __init__\u201d as the default way to\nallow users to create your objects, we make a habit of beginning every class\nwith a pile of arbitrary code that gets executed every time it is\ninstantiated.\nWherever there is arbitrary code, there are arbitrary problems.\nThe Problems\nLet\u2019s consider a data structure more complex than one that simply holds a\ncouple of attributes.  We will create one that represents a reference to some\nI/O in the external world: a FileReader.\nOf course Python has its own open-file object\nabstraction, but I\nwill be ignoring that for the purposes of the example.\nLet\u2019s assume a world where we have the following functions, in an imaginary\nfileio module:\n\nopen(path: str) -> int\nread(fileno: int, length: int)\nclose(fileno: int)\n\nOur hypothetical fileio.open returns an integer representing a file\ndescriptor1, fileio.read allows us to read length bytes from an open\nfile descriptor, and fileio.close closes that file descriptor, invalidating\nit for future use.\nWith the habit that we have built from writing thousands of __init__ methods,\nwe might want to write our FileReader class like this:\nclass FileReader:\n    def __init__(self, path: str) -> None:\n        self._fd = fileio.open(path)\n    def read(self, length: int) -> bytes:\n        return fileio.read(self._fd, length)\n    def close(self) -> None:\n        fileio.close(self._fd)\n\n\nFor our initial use-case, this is fine.  Client code creates a FileReader by\ndoing something like FileReader(\"./config.json\"), which always creates a\nFileReader that maintains its file descriptor int internally as private\nstate.  This is as it should be; we don\u2019t want user code to see or mess with\n_fd, as that might violate FileReader\u2019s invariants.  All the necessary work\nto construct a valid FileReader \u2014 i.e. the call to open \u2014 is always taken\ncare of for you by FileReader.__init__.\nHowever, additional requirements will creep in, and as they do,\nFileReader.__init__ becomes increasingly awkward.\nInitially we only care about fileio.open, but later, we may have to deal with\na library that has its own reasons for managing the call to fileio.open by\nitself, and wants to give us an int that we use as our _fd, we now have to\nresort to weird workarounds like:\ndef reader_from_fd(fd: int) -> FileReader:\n    fr = object.__new__(FileReader)\n    fr._fd = fd\n    return fr\n\n\nNow, all those nice properties that we got from trying to force object\nconstruction to give us a valid object are gone.  reader_from_fd\u2019s type\nsignature, which takes a plain int, has no way of even suggesting to client\ncode how to ensure that it has passed in the right kind of int.\nTesting is much more of a hassle, because we have to patch in our own copy of\nfileio.open any time we want an instance of a FileReader in a test without\ndoing any real-life file I/O, even if we could (for example) share a single\nfile descriptor among many FileReader s for testing purposes.\nAll of this also assumes a fileio.open that is synchronous.  Although for\nliteral file I/O this is more of a\nhypothetical\nconcern, there are many types of networked resource which are really only\navailable via an asynchronous (and thus: potentially slow, potentially\nerror-prone) API.  If you\u2019ve ever found yourself wanting to type async def\n__init__(self): ... then you have seen this limitation in practice.\nComprehensively describing all the possible problems with this approach would\nend up being a book-length treatise on a philosophy of object oriented design,\nso I will sum up by saying that the cause of all these problems is the same:\nwe are inextricably linking the act of creating a data structure with\nwhatever side-effects are most often associated with that data structure.\nIf they are \u201coften\u201d associated with it, then by definition they are not\n\u201calways\u201d associated with it, and all the cases where they aren\u2019t associated\nbecome unweildy and potentially broken.\nDefining an __init__ is an anti-pattern, and we need a replacement for it.\nThe Solutions\nI believe this tripartite assemblage of design techniques will address the\nproblems raised above:\n\nusing dataclass to define attributes,\nreplacing behavior that previously would have previously been in __init__\n  with a new classmethod that does the same thing, and\nusing precise types to describe what a valid instance looks like.\n\nUsing dataclass attributes to create an __init__ for you\nTo begin, let\u2019s refactor FileReader into a dataclass.  This does get us an\n__init__ method, but it won\u2019t be one an arbitrary one we define ourselves;\nit will get the useful constraint enforced on it that it will just assign\nattributes.\n@dataclass\nclass FileReader:\n    _fd: int\n    def read(self, length: int) -> bytes:\n        return fileio.read(self._fd, length)\n    def close(self) -> None:\n        fileio.close(self._fd)\n\n\nExcept... oops.  In fixing the problems that we created with our custom\n__init__ that calls fileio.open, we have re-introduced several problems\nthat it solved:\n\nWe have removed all the convenience of FileReader(\"path\").  Now the user\n   needs to import the low-level fileio.open again, making the most common\n   type of construction both more verbose and less discoverable; if we want\n   users to know how to build a FileReader in a practical scenario, we will\n   have to add something in our documentation to point at a separate module\n   entirely.\nThere\u2019s no enforcement of the validity of _fd as a file descriptor; it\u2019s\n   just some integer, which the user could easily pass an incorrect instance\n   of, with no error.\n\nIn isolation, dataclass by itself can\u2019t solve all our problems, so let\u2019s add\nin the second technique.\nUsing classmethod factories to create objects\nWe don\u2019t want to require any additional imports, or require users to go looking\nat any other modules \u2014 or indeed anything other than FileReader itself \u2014 to\nfigure out how to create a FileReader for its intended usage.\nLuckily we have a tool that can easily address all of these concerns at once:\n@classmethod.  Let\u2019s define a FileReader.open class method:\nfrom typing import Self\n@dataclass\nclass FileReader:\n    _fd: int\n    @classmethod\n    def open(cls, path: str) -> Self:\n        return cls(fileio.open(path))\n\n\nNow, your callers can replace FileReader(\"path\") with\nFileReader.open(\"path\"), and get all the same benefits.\nAdditionally, if we needed to await fileio.open(...), and thus we needed its\nsignature to be @classmethod async def open, we are freed from the constraint\nof __init__ as a special method.  There is nothing that would prevent a\n@classmethod from being async, or indeed, from having any other\nmodification to its return value, such as returning a tuple of related values\nrather than just the object being constructed.\nUsing NewType to address object validity\nNext, let\u2019s address the slightly trickier issue of enforcing object validity.\nOur type signature calls this thing an int, and indeed, that is unfortunately\nwhat the lower-level fileio.open gives us, and that\u2019s beyond our control.\nBut for our own purposes, we can be more precise in our definitions, using\nNewType:\nfrom typing import NewType\nFileDescriptor = NewType(\"FileDescriptor\", int)\n\n\nThere are a few different ways to address the underlying library, but for the\nsake of brevity and to illustrate that this can be done with zero run-time\noverhead, let\u2019s just insist to Mypy that we have versions of fileio.open,\nfileio.read, and fileio.write which actually already take FileDescriptor\nintegers rather than regular ones.\nfrom typing import Callable\n_open: Callable[[str], FileDescriptor] = fileio.open  # type:ignore[assignment]\n_read: Callable[[FileDescriptor, int], bytes] = fileio.read\n_close: Callable[[FileDescriptor], None] = fileio.close\n\n\nWe do of course have to slightly adjust FileReader, too, but the changes are\nvery small.  Putting it all together, we get:\nfrom typing import Self\n@dataclass\nclass FileReader:\n    _fd: FileDescriptor\n    @classmethod\n    def open(cls, path: str) -> Self:\n        return cls(_open(path))\n    def read(self, length: int) -> bytes:\n        return _read(self._fd, length)\n    def close(self) -> None:\n        _close(self._fd)\n\n\nNote that the main technique here is not necessarily using NewType\nspecifically, but rather aligning an instance\u2019s property of \u201chas all attributes\nset\u201d as closely as possible with an instance\u2019s property of \u201cfully valid\ninstance of its class\u201d; NewType is just a handy tool to enforce any necessary\nconstraints on the places where you need to use a primitive type like int,\nstr or bytes.\nIn Summary - The New Best Practice\nFrom now on, when you\u2019re defining a new Python class:\n\nMake it a dataclass2.\nUse its default __init__ method3.\nAdd @classmethods to provide your users convenient and discoverable ways to\n  build your objects.\nRequire that all dependencies be satisfied by attributes, so you always\n  start with a valid object.\nUse typing.NewType to enforce any constraints on primitive data types (like\n  int and str) which might have magical external attributes, like needing\n  to come from a particular library, needing to be random, and so on.\n\nIf you define all your classes this way, you will get all the benefits of a\ncustom __init__ method:\n\nAll consumers of your data structures will receive valid objects, because an\n  object with all its attributes populated correctly is inherently valid.\nUsers of your library will be presented with convenient ways to create your\n  objects that do as much work as is necessary to make them easy to use, and\n  they can discover these just by looking at the methods on your class itself.\n\nAlong with some nice new benefits:\n\nYou will be future-proofed against new requirements for different ways that\n    users may need to construct your object.\nIf there are already multiple ways to instantiate your class, you can now\n    give each of them a meaningful name; no need to have monstrosities like\n    def __init__(self, maybe_a_filename: int | str | None = None):\nYour test suite can always construct an object by satisfying all its\n    dependencies; no need to monkey-patch anything when you can always call the\n    type and never do any I/O or generate any side effects.\n\nBefore dataclasses, it was always a bit weird that such a basic feature of the\nPython language \u2014 giving data to a data structure to make it valid \u2014 required\noverriding a method with 4 underscores in its name.  __init__ stuck out like\na sore thumb.  Other such methods like __add__ or even __repr__ were\ninherently customizing esoteric attributes of classes.\nFor many years now, that historical language wart has been\nresolved. @dataclass, @classmethod, and NewType give you everything you\nneed to build classes which are convenient, idiomatic, flexible, testable, and\nrobust.\n\nAcknowledgments\nThank you to my patrons who are supporting my writing on\nthis blog.  If you like what you\u2019ve read here and you\u2019d like to read more of\nit, or you\u2019d like to support my various open-source\nendeavors, you can support my work as a\nsponsor!  I am also available for\nconsulting work if you think your organization\ncould benefit from expertise on topics like \u201cbut what is a \u2018class\u2019, really?\u201d."
                ],
                "output": "readability/",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:35.966225+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2025-12-16T10:32:35.126795+00:00",
                "index_texts": null,
                "output": "Deciphering Glyph ::\n        Stop Writing `__init__` Methods",
                "pwd": "/data/archive/1765881132.523814",
                "schema": "ArchiveResult",
                "start_ts": "2025-12-16T10:32:35.099332+00:00",
                "status": "succeeded"
            }
        ],
        "wget": []
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": "TimeoutExpired: Command '['/usr/bin/curl', '--silent', '--location', '--compressed', '--proxy', 'socks5://tor-socks-proxy:9150', '--head', '--max-time', '60', '--user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)', 'https://web.archive.org/save/https://blog.glyph.im/2025/04/stop-writing-init-methods.html']' timed out after 60 seconds",
        "dom": "output.html",
        "favicon": "favicon.ico",
        "git": null,
        "media": "media/",
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Deciphering Glyph ::\n        Stop Writing `__init__` Methods",
        "warc": null,
        "wget": null
    },
    "link_dir": "/data/archive/1765881132.523814",
    "newest_archive_date": "2025-12-16T10:33:14.483627+00:00",
    "num_failures": 1,
    "num_outputs": 8,
    "oldest_archive_date": "2025-12-16T10:32:19.224941+00:00",
    "path": "/2025/04/stop-writing-init-methods.html",
    "schema": "Link",
    "scheme": "https",
    "snapshot_abid": "snp_01KCKBFDHK3F4C84AB013NWT84",
    "snapshot_id": "8af5ac48-d635-40e4-b230-70fc475e6904",
    "sources": [
        "/data/sources/1765881131-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1765881132.523814",
    "title": "Deciphering Glyph ::\n        Stop Writing `__init__` Methods",
    "url": "https://blog.glyph.im/2025/04/stop-writing-init-methods.html"
}