Skip to content

1.0.P Design Patterns

Julio Toboso edited this page Oct 26, 2026 · 7 revisions

Patterns

Each pattern has the same shape: when you want something, write this, and watch out for that.

Pattern 7 · Keywords for rules written as data

When you want rules that live in tables or files and check words, not classes. Mark the Tags that are words with @Flag.

@Flag
class Undead(Tag):
    pass


@Flag
class Flying(Tag):
    pass


ghoul = Character("Ghoul")
Undead(ghoul)

assert "Undead" in ghoul                # by name
assert Undead in ghoul                  # by class
assert "Flying" not in ghoul

TURNABLE = ["Undead", "Fiend"]          # a rule as data
assert any(word in ghoul for word in TURNABLE)

assert Keyword(ghoul, "Undead")         # the function form: any object
assert not Keyword(Character("x"), "Undead")

When one word is not enough, list the others. The Tag's own name always counts; the words are its variants and neighbours:

@Flag("Wolf", "Lycanthrope", "Beast")
class Werewolf(Tag):
    pass


howler = Character("Howler")
Werewolf(howler)

assert "Wolf" in howler                 # a word
assert "Werewolf" in howler             # the name, always
assert Keyword(howler, "Beast", "Lycanthrope")

HUNTED = ["Beast", "Fiend"]             # the table never imports Werewolf
assert any(word in howler for word in HUNTED)

A word is a public name, not a membership: if there is also a Beast Tag, "Beast" in howler is True while howler in Beast is False. That is the point, a secret identity with a public face, but it is yours to keep straight. When a word needs a Field to walk, a gate or a promise, make it a Tag and a Base (class Werewolf(Beast)); when rules only match it, a word is enough.

Watch out. A string never asks membership. An ordinary Tag never answers to its name: "Wizard" in agent says nothing about being a Wizard, and is True only while an active Flag is named Wizard or lists the word. Words are strings, @Flag("Beast"); @Flag(Beast) is the bare form applied to the class Beast, not a word.

One seat, one meaning. A Flag cannot join an object that already answers in: a list, a party that iterates its members (__iter__), or an Agent carrying a Tag with a __contains__ or __iter__ Action. Nor can such a Tag join an Agent that carries a Flag. TopKit refuses either way, and the message names both sides, so "alice" in party never changes meaning in silence. A keyed sheet (sheet["STR"], only __getitem__) has no working in, and a Flag may take it. So may an old-style sequence that answers only line[0], line[1], …, and there "alice" in line stops meaning membership: give such a class a real __iter__ if its in matters.

A class that writes __contains__ = None tells Python its in is unavailable, and a Flag may then take the seat. The line belongs to the whole class, not to one Agent. Once a Flag lands, the class's own x in self answers keywords, and so do inherited container methods that ask in (the comparisons of a collections.abc.Set). Write it only for a class whose in nobody uses for membership.

Pattern 8 · Shared things, and who may reach them

When you want one value or one behaviour for the whole Field, and a clean line between what the Agent shows and what the Tag keeps.

class Agency(Tag):

    @Public
    @Report
    def colour(tag):
        return "navy"                   # one copy, published on the Agent

    @Report
    def HQ(tag):
        return "London"                 # one copy, Tag-side only

    @Public
    @Operation
    def dispatch(agency, sender, message):
        return f"{sender.name}: {message}"   # only members ever get here

    @Secret
    @Record
    def clearance(agent):
        return "top"

    @Action
    def status(agent):
        return agent.clearance          # inside: the secret resolves


bond = Character("Bond")
Agency(bond)

assert bond.colour == "navy"            # published Report, read-only
assert bond.dispatch("hello") == "Bond: hello"
assert bond.status() == "top"
assert not hasattr(bond, "HQ")          # Tag-side stays Tag-side
assert not hasattr(bond, "clearance")   # secret from outside

send = bond.dispatch                    # a captured handle
del Agency[bond]

assert bond.status() == "top"           # his own Action: he keeps it

try:
    send("late")
except TagResolutionError:
    pass                                # the Agency's: revoked with membership

The rule in one line: what the Agent does is public, what the Agency keeps is internal. @Secret hides an Agent member; @Public shows a Tag member. Both are modifiers: they stack on @Record, @Action, @Report or @Operation in either order.

A published member answers members only, and only sound ones. A Rogue Agent (he left) keeps what he became, his own Actions and Records, and loses what the Agency lent him: TagRogueAccessError. A defective Agent (a promise broke, any promise, whoever made it) is turned away until repaired, and the refusal names the broken promise. You never write either check, and the second one gives you the autofix pattern: catch the promise, repair what it names, try again.

class Sworn(Tag):

    @Post
    def Has_Oath(agent):
        return agent.oath is not None


Agency(bond)
bond.oath = None
try:
    Sworn(bond)
except Postcondition.Has_Oath:
    pass                                # a member, but defective

try:
    bond.dispatch("hello")
except Postcondition.Has_Oath:          # the refusal names what is broken
    bond.oath = "for Queen and country"
    assert bond.dispatch("hello") == "Bond: hello"

Notice the symmetry. A Report is written exactly like a Record, with the Tag instead of the Agent as its first input; it runs once per Tag and its value is shared by the whole Field. def hit_die(tag, inherited) extends the Base's value, as def spells(agent, stored) extends what is stored.

Watch out. A secret resolves only while one of the Agent's own Actions or protocols is running. A handle to a secret Action captured inside and called outside fails, on purpose.

Pattern 9 · Roles that end

When you want a Tag to clean up when it leaves, or a role that lasts for a block of code.

class Sentry(Tag):

    @Imprint
    def post(agent):
        agent.on_duty = True

    @Rip
    def stand_down(agent):
        agent.on_duty = False


guard = Character("Guard")

with Scope(guard, Sentry):              # applies on entry
    assert guard.on_duty

assert not guard.on_duty                # Ripped on exit, even on error
assert guard not in Sentry

@Imprint runs after the Tag applies; @Rip runs after it leaves. They are constructor and destructor, __enter__ and __exit__.

A role's conditions do not leave with it on their own. What the Agent became stays (pattern 1's Rogue Agent), and so does what the role required: a promise that outlives its Tag fails loud, never silently. If a promise should end with the role, you say so, in the role's own @Rip protocol, one name at a time:

class Knight(Tag):

    @Post
    def Has_Sword(agent):
        return agent.sword is not None

    @Rip
    def Undub(agent):
        Contract.Delete(agent, "Has_Sword")    # the promise ends here, visibly

    def Salute(agent):
        return f"{agent.name} salutes"


lance = Character("Lance")
lance.sword = "Excalibur"
Knight(lance)

del Knight[lance]                       # dubbed no more
lance.sword = None

assert lance.Salute() == "Lance salutes"   # what he became: stays
assert lance                               # what the role required: ended by you

The other way is a guard inside the promise, if agent not in Knight: return True, which lets the promise follow any membership you like; the Contracts Guide shows both.

Watch out. Python does not promise to run finalizers at shutdown, so del agent is best-effort. Scope is the guaranteed path.

An object's own __del__ is a Layer. It keeps running when the object is tagged. A Tag may replace it, or wrap it with @Underlay; the Tags' teardowns always run first, and a __del__ never stops them:

farewells = []


class Lantern:
    def __del__(self):
        farewells.append("wick out")


class Carried(Tag):
    @Rip
    def Put_Down(agent):
        farewells.append("put down")


class Enchanted(Tag):
    @Underlay
    def __del__(agent, underlay):
        farewells.append("spell fades")
        underlay()                          # the Lantern's own, or nothing


lamp = Lantern()
Carried(lamp)
Enchanted(lamp)
del lamp

assert farewells == ["put down", "spell fades", "wick out"]

At interpreter exit only the __del__ Layers run. To have teardowns run at exit too, register the Agent with At_Exit(agent).

Pattern 10 · Build the sheet from pieces

Put it together. This is the D&D example in miniature.

class Species(Tag):

    @Record
    def speed(agent):
        return 30


class Elf(Species):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Light"]


class Class(Tag):

    @Report
    def hit_die(tag):
        return 8


class Wizard(Class):

    @Report
    def hit_die(tag):
        return 6

    @Pre
    def Can_Study(agent):
        return agent.level >= 1

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Magic Missile"]

    @Record
    def hit_points(agent, stored):
        return (stored or 0) + agent.level * Wizard.hit_die

    @Post
    def Knows_Spells(agent):
        return len(agent.spells) > 0

    def Attack(agent):
        return f"{agent.name} casts {agent.spells[-1]}"


class Fighter(Tag):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Identify"]


class War_Caster(Tag):

    @Pre
    def Is_A_Caster(agent):
        return agent in Wizard

    @Action
    @Underlay
    def Attack(agent, underlay):
        return underlay() + " behind a shield"


ari = Character("Ari", level=3)

for choice in (Elf, Wizard, Fighter, War_Caster):
    choice(ari)

assert ari.spells == ["Light", "Magic Missile", "Identify"]
assert ari.hit_points == 18
assert ari.Attack() == "Ari casts Identify behind a shield"
assert ari                                      # every promise holds
assert f"{ari:tags}" == "Elf, Wizard, Fighter, War_Caster"

print(Outline(ari))
# Character
#   Species
#     Elf
#   Class
#     Wizard
#   Fighter
#   War_Caster

Pattern 11 · Categories of categories

Categories can themselves be categorized: a class may be rare, a species homebrew, or a rule deprecated. A Pin applies such meaning to a Tag. Rare(Wizard) classifies Wizard among the rare Tags; Wizard remains a Tag with its own Agents. The pinned Tag is the Pin’s Target.

class Rare(Pin):

    @Record
    def rarity(tag):                        # the receiver is the Tag
        return "rare"

    @Action
    def Describe(tag):
        return f"{tag.__name__} is {tag.rarity}"


Rare(Wizard)                                # pin the Tag

assert Wizard in Rare                       # membership, from the Pin's side
assert list(Rare) == [Wizard]               # a Field of Tags
assert Wizard.rarity == "rare"              # one value, held on the Tag
assert Wizard.Describe() == "Wizard is rare"
assert not hasattr(ari, "rarity")           # never on the Tag's Agents

for tag in Rare:                            # walk the rare Tags
    print(tag.Describe())

The receiver is named tag in these examples because the Record builder and Action operate on the targeted Tag. The Record supplies a Report, and the Action supplies an Operation: information and behaviour held at Tag scope. Conditions have a different subject. A Precondition or Postcondition contributed through a Pin becomes a condition the target Tag applies to its Agents, and receives the Agent being checked.

A Pin can also patch what a Tag shares. Say a Tag has an Operation that controls an engine, and the engine changed. A Pin Action of the same name replaces it, with the old one as its Underlay, and every Agent that goes through the Tag gets the new control at once:

class Engine(Tag):

    @Operation
    def Control(tag, level):
        return f"{tag.__name__} v1 at {level}"

    def Drive(agent):
        return Engine.Control(agent.level)      # through the Tag, at call time


kit = Character("Kit", level=2)
Engine(kit)
assert kit.Drive() == "Engine v1 at 2"


@Pin
class Firmware_2(Tag):

    @Action
    @Underlay
    def Control(tag, underlay, level):
        return underlay(level).replace("v1", "v2")

    @Public                                 # reach every Agent of the Tag, too
    @Record
    def firmware(tag):
        return "v2"

    @Rip
    def Unpatch(tag, original):             # the originals, handed to you
        tag.Control = original.Control


Firmware_2(Engine)

assert kit.Drive() == "Engine v2 at 2"      # actualized, no per-Agent work
assert kit.firmware == "v2"                 # published onto the Field

del Firmware_2[Engine]                      # un-patch: one deliberate line
assert kit.Drive() == "Engine v1 at 2"

Keep the scopes distinct. A Pin’s Records and Actions modify what the target Tag shares as Reports and Operations; these contributions must not accidentally overwrite the Tag’s Agent declarations. A Pin’s Preconditions and Postconditions contribute to the conditions declared for those Agents. They do not create promises that the Tag itself must satisfy.


Reading an Agent

Question Spelling
Is it a Wizard now? agent in Wizard
Was it ever? isinstance(agent, Wizard)
Does it carry the keyword? "Undead" in agent, Keyword(agent, "Undead"), a Flag's words too
Which Tags, in order? Tags(agent), f"{agent:tags}"
The whole shape? Outline(agent), f"{agent:outline}"
Are its promises holding? if agent:, Contract.Status(agent), f"{agent:contract}"
What did it look like right after Wizard? agent.Wizard, Wizard[agent]

Reading a Tag

Question Spelling
Who is one, and sound? for a in Wizard, len(Wizard), if Wizard:
Who is one, and broken? for a in ~Wizard, if ~Wizard:
Everyone? Wizard[:]
Either, both, without? Wizard | Fighter, Wizard & Fighter, Wizard - Sworn (the Fields Guide)
What applies with it? Form(Wizard), f"{Wizard:form}"
Take it away del Wizard[agent]
Which Pins does it carry? Wizard in Rare, f"{Wizard:pins}"

Nothing TOP-level lives at Wizard.something. That namespace is yours: put your Reports and Operations there.


Do and don't

Do Don't
Make a Tag for what something is: species, class, role, clearance. Make a Tag for a passing state. Asleep is a Record, asleep = True.
Let independent Tags pile up on one Record with stored. Chain Tags by inheritance just to share a list.
Extend an Action with @Underlay. Call agent.Attack() from inside Attack (that recurses).
Gate with @Pre; promise with @Post; repair through ~Tag. Roll your own validation after the fact.
Mark word-like Tags @Flag and write rules as data. Match every Tag by name. Only Flags are words.
Say things about a Tag with a @Pin. Keep a side table of Tags outside TOP.
Clean up with @Rip, guarantee it with Scope. Rely on del agent for anything that matters.
Reset by Rip and apply again. Reapply an active Tag hoping it resets (it does nothing).
Expect a Rogue Agent to keep its own Actions and lose the Agency's published ones. Check membership by hand inside every published Operation.
Catch the named promise, repair what it names, retry. Catch every failure in one handler and guess.
Keep shared data in a Report, one copy. Copy shared data into every Agent's Record.
Ask agent in Wizard. Ask type(agent) is Character; the type is wrapped.

Where next

Clone this wiki locally