Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

wakatime Tokens codecov PyPI version License: Apache 2.0

worktoy v1.0.1-dev0 (development build)

worktoy provides utilities for Python development focused on reducing boilerplate while improving type safety and readability. Each release is tested thoroughly on each supported Python version from 3.7* to 3.14.

*Maybe it is time to consider updating if you are still using Python 3.7.

Table of Contents

Installation

Install with pip:

pip install worktoy

Python Is Easy. Too Easy!

What enables effortless prototyping does not guarantee the scalable structure serious applications demand.

Introducing worktoy.

A structural layer for Python that adds deliberate constraints without sacrificing ergonomics. It brings the architectural discipline of statically typed languages to dynamic Python.

Build the GUI. Build the logic. Build the architecture. All in Python.

'Trust-Me-Bro'-Typing

class Point:
  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

The above code is perfectly valid Python, it even includes types. Or does it? Those float annotations are not there at runtime. Basically, it is 'trust-me-bro'-typing. Point('breh', None) will happily create a Point object.

Instead:

from worktoy.desc import AttriBox


class Point:
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

When AttriBox says float it enforces float at runtime. Attributes are declared explicitly at the class level. Despite this, flexibility remains, for example:

point = Point(69, '420')  # int, str
point.x == 69.0
point.y == 420.0

When types do not match, AttriBox attempts casting before raising an error. Same ergonomics. Stronger guarantees.

Note that Point above is a plain class: both AttriBox and Field are ordinary descriptors and work on any class. The BaseObject base shown in the examples below is required only for the @overload machinery, not for declarative attributes.

The Python Parsing Situation Is Crazy

Python is not always easy though. Consider the Point implementation under discussion. Suppose we wanted a flexible constructor. One that supports instantiation on:

  • a pair of float objects
  • a complex number
  • another Point object

That is possible in Python, for example:

class Point:

  def __init__(self, *args, ) -> None:
    if len(args) == 2:
      self.x = float(args[0])
      self.y = float(args[1])
    elif len(args) == 1:
      if isinstance(args[0], complex):
        self.x = args[0].real
        self.y = args[0].imag
      elif isinstance(args[0], type(self)):
        self.x = args[0].x
        self.y = args[0].y
    else:
      raise TypeError('Invalid arguments')

Possible? Sure, but look at that syntactic broccoli! Conditional branches. Growing complexity. Manual parsing. Long gone are those happy days of effortless coding.

But it does not have to be like this. Introducing @overload:

from __future__ import annotations

from typing import TYPE_CHECKING

from worktoy.mcls import BaseObject
from worktoy.core.sentinels import THIS
from worktoy.dispatch import overload
from worktoy.desc import AttriBox

if TYPE_CHECKING:  # pragma: no cover
  from typing import Self


class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  @overload(float, float)
  def __init__(self, x: float, y: float) -> None:
    self.x = x
    self.y = y

  @overload(complex)
  def __init__(self, z: complex) -> None:
    self.x = z.real
    self.y = z.imag

  @overload(THIS)  # THIS = the enclosing class (matches an instance of it)
  def __init__(self, other: Self) -> None:
    self.x = other.x
    self.y = other.y

  @overload()
  def __init__(self, ) -> None:
    pass

Each new signature requires one new overloaded function. No more painful parsing of *args. All of this just works. Actually.

"Show Don't Tell" Is for Stories, not for Code!

When reading code, you look for declarations. For where symbols are defined. For where meaning begins.

Narrative storytelling is different. The method by which information is conveyed is itself part of the artistic expression. The way information is revealed is frequently as important as the information itself. In Clair Obscur: Expedition 33, the horror of the Gommage unfolds gradually until Sophie disappears from Gustave's arms. The imperative subtlety grants the story its emotional impact.

In code, the declaration is the point! In matters of code, I want declarations. I don't want foreshadowing. I don't want subtlety. I don't want subversion of expectations. I want declarations.

Anyway, what were we talking about? Right, figure out what point.r is from the code below:

class Point:
  def __init__(self, *args, ) -> None:
    if len(args) == 2:
      self.x = float(args[0])
      self.y = float(args[1])
    elif len(args) == 1:
      if isinstance(args[0], complex):
        self.x = args[0].real
        self.y = args[0].imag
      elif isinstance(args[0], type(self)):
        self.x = args[0].x
        self.y = args[0].y
    else:
      raise TypeError('Invalid arguments')

  @property
  def r(self) -> float:
    return (self.x ** 2 + self.y ** 2) ** 0.5

Great, you found it. Well, you found what it does, and you inferred it. This is imperative declaration. In Python, this is fine. It is much worse in other languages. Anyway, here is the alternative provided by worktoy: Field.

from worktoy.desc import Field


class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  r: Field[float] = Field()  # Straight up declaration!

  @overload(float, float)
  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

  @overload(complex)
  def __init__(self, z: complex) -> None:
    self.x = z.real
    self.y = z.imag

  @overload(THIS)  # THIS = the enclosing class (matches an instance of it)
  def __init__(self, other: Self) -> None:
    self.x = other.x
    self.y = other.y

  @overload()
  def __init__(self, ) -> None:
    pass

  @r.GET  # Straight up declaration of something called 'GET'.
  def _getR(self) -> float:
    return (self.x ** 2 + self.y ** 2) ** 0.5

The r attribute is declared first. Next, the @r.GET declares that the method implementing the get operation comes next. The structure is visible separately from the behaviour.

Static Discipline

In plain Python, attributes assigned in __init__ are closer to dictionary entries than declared structure.

class Point:
  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

Inspecting the class reveals nothing about x or y. They do not exist at the class level. They are created at runtime on the instance. Two common remedies are __slots__ and annotations:

class Point:
  __slots__ = ('x', 'y')

  def __init__(self, *args) -> None: ...

or

class Point:
  x: float
  y: float

  def __init__(self, *args, ) -> None: ...  # Implementation as before

Both improve clarity. But in both cases Point.x and Point.y will raise AttributeError. The presence of x and y becomes visible only after instantiation. At this point, they are just attributes of the instance, not of the class. Setting during __init__ makes no difference compared to setting them anywhere else. Structure remains implicit.

With worktoy attributes are an essential part of the class structure on par with methods.

class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  #  Implementation as before

Now x and y are declared at the class level, making them visible, inspectable and enforced. They are more than just keys in an instance dictionary. They are structural elements of the class. In plain Python, instances define structure. Here, the class does.

Contributing

Contributions are welcome. See CONTRIBUTING.md for the style rules and development setup.

License

worktoy is released under the Apache License 2.0 (Apache-2.0). See LICENSE for the full text.

Release files for worktoy 1.0.1.dev0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for worktoy 1.0.1.dev0
File Size Uploaded
worktoy-1.0.1.dev0.tar.gz 166.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for worktoy 1.0.1.dev0
File Interpreter ABI Platform
worktoy-1.0.1.dev0-py3-none-any.whl Python 3 none any Details

Total release size: 412.2 kB

Release files / worktoy-1.0.1.dev0.tar.gz

Download URL worktoy-1.0.1.dev0.tar.gz
Size 166.0 kB
Tags Source
SHA-256 checksum
How to use checksums
07f7c5f63818c79e111b87ca704403c5c2fc460e20b151d4c91aee52c4a6a4b9
BLAKE2b-256 checksum
How to use checksums
5091e21ba45726dbaf103145fd6731028be9cfa294ea57de6d8cc14e035a953f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / worktoy-1.0.1.dev0-py3-none-any.whl

Download URL worktoy-1.0.1.dev0-py3-none-any.whl
Size 246.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ddc6b37d1d30842e188438ee4556c2e4000191a164d36263cc9275efb1c32735
BLAKE2b-256 checksum
How to use checksums
c207f098ffb73cecac0686ada91c8fa7d0dfc6d9cd0fcf16f237ef2373f91bb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1.dev0 This release

2 release files

1.0.0

2 release files

0.99.9

2 release files

0.99.8

2 release files

0.99.7

2 release files

0.99.6

2 release files

0.99.5

2 release files

0.99.4

2 release files

0.99.3

2 release files

0.99.2

2 release files

0.99.1

2 release files

0.49.7

2 release files

0.40.2

2 release files

0.40.1

2 release files

0.31.2

2 release files

0.31.1

2 release files

0.31.0

2 release files

0.30.0

2 release files

0.28.8

2 release files

0.28.4

2 release files

0.28.3

2 release files

0.28

2 release files

0.25

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page