<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Packt Deep Engineering: Tutorials]]></title><description><![CDATA[Practical, hands-on guides from Packt authors and experts—designed to help you apply modern tools, techniques, and patterns in real-world systems.]]></description><link>https://deepengineering.net/s/tutorials</link><image><url>https://substackcdn.com/image/fetch/$s_!H5BJ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736bc1ee-d689-497e-83a8-7d9bf9022eb9_600x600.png</url><title>Packt Deep Engineering: Tutorials</title><link>https://deepengineering.net/s/tutorials</link></image><generator>Substack</generator><lastBuildDate>Sun, 28 Jun 2026 17:17:16 GMT</lastBuildDate><atom:link href="https://deepengineering.net/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Packt]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[deepengineering@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[deepengineering@substack.com]]></itunes:email><itunes:name><![CDATA[Packt]]></itunes:name></itunes:owner><itunes:author><![CDATA[Packt]]></itunes:author><googleplay:owner><![CDATA[deepengineering@substack.com]]></googleplay:owner><googleplay:email><![CDATA[deepengineering@substack.com]]></googleplay:email><googleplay:author><![CDATA[Packt]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[From Java to Kotlin: A Large-Scale Migration Story with AI Assistance]]></title><description><![CDATA[Why Migrate to Kotlin in Modern Java Codebases?]]></description><link>https://deepengineering.net/p/from-java-to-kotlin-a-large-scale</link><guid isPermaLink="false">https://deepengineering.net/p/from-java-to-kotlin-a-large-scale</guid><dc:creator><![CDATA[Ron veen]]></dc:creator><pubDate>Wed, 18 Mar 2026 18:09:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a3586000-b386-4273-b570-744b0f0b7291_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I had to migrate a Java codebase to Kotlin. Now, we are talking about proprietary software, so the code cannot be shared in this article. Thus, I decided to find an open-source project with approximately the same characteristics. That is the reason why we use OpenMRS in this article.</p><p>The OpenMRS project, a robust open-source medical records system serving healthcare providers worldwide, has been built on Java for over a decade. With hundreds of thousands of lines of code across domain models, service layers, data access objects, and utilities, the codebase represents years of careful engineering and community contribution.</p><p>So why consider migrating to Kotlin?</p><p>The decision wasn&#8217;t taken lightly. Kotlin offers several compelling advantages that align with modern software development practices:</p><ul><li><p><strong>Null Safety:</strong> Kotlin&#8217;s type system distinguishes between nullable and non-nullable types at compile time, eliminating the infamous <code>NullPointerException</code> that plagues Java applications. In a medical records system where data integrity is paramount, this additional safety net is invaluable.</p></li><li><p><strong>Conciseness:</strong> Kotlin reduces boilerplate code significantly. Properties replace getter/setter pairs, data classes eliminate constructor and method boilerplate, and smart casts remove redundant type checks. Our migration showed an average code reduction of 20-47% across different layers.</p></li><li><p><strong>100% Java Interoperability:</strong> Kotlin compiles to the same bytecode as Java and can seamlessly call Java code and vice versa. This meant we could migrate incrementally, file by file, without breaking existing functionality.</p></li><li><p><strong>Modern Language Features:</strong> Extension functions, coroutines, sealed classes, and expressive lambda syntax make code more readable and maintainable.</p></li><li><p><strong>Industry Adoption:</strong> Google declared Kotlin the preferred language for Android development, and it has gained significant traction in server-side development, with frameworks like Spring Boot fully supporting Kotlin.</p></li></ul><h3>Why Use AI for Large-Scale Migration?</h3><p>Migrating a codebase of this magnitude manually would be a herculean task. While IntelliJ IDEA provides excellent Java-to-Kotlin conversion tools, they produce mechanical translations that don&#8217;t take full advantage of Kotlin&#8217;s idioms. The converted code works, but it doesn&#8217;t read like idiomatic Kotlin.</p><p>This is where AI assistance becomes transformative:</p><ul><li><p><strong>Pattern Recognition:</strong> AI can recognize common Java patterns (like builder patterns, static utility classes, or verbose null checks) and replace them with idiomatic Kotlin equivalents (apply blocks, object declarations, safe calls).</p></li><li><p><strong>Consistency:</strong> Across hundreds of files, maintaining consistent style and patterns is crucial. AI applies the same transformation rules uniformly, ensuring the converted codebase feels cohesive.</p></li><li><p><strong>Context Awareness:</strong> AI can understand the broader context&#8212;whether a class needs to be <code>open</code> for Spring proxying, whether properties should be <code>lateinit</code> or nullable, and how to preserve framework annotations properly.</p></li><li><p><strong>Iterative Refinement:</strong> Unlike one-shot conversion tools, AI-assisted migration allows for learning and improvement. Early migrations establish patterns that are refined and applied consistently across subsequent conversions.</p></li><li><p><strong>Scale:</strong> Manually reviewing and converting 250+ files while maintaining quality would take weeks or months. AI assistance can process multiple files per hour while maintaining high standards.</p></li></ul><h3>Why Claude Code?</h3><p>The choice of AI tool was the first decision to be made. There are several candidates:</p><ul><li><p>Anthropic Claude Code</p></li><li><p>Google Gemini CLI</p></li><li><p>OpenAI Codex</p></li><li><p>Microsoft CoPilot</p></li><li><p>OpenCode</p></li><li><p>Cursor</p></li></ul><p>Most of these are so-called command-line tools, except Cursor. Choosing a model is very personal, based on one&#8217;s own experiences. It can easily lead to flame wars, like IntelliJ vs. Eclipse, or tabs vs. spaces.</p><p>Anthropic&#8217;s models differ from models like ChatGPT and Gemini in that the latter are mainly general AI models, while the former have a specific focus on software development. Anthropic offers 3 models:</p><ul><li><p>Haiku</p></li><li><p>Opus</p></li><li><p>Sonnet</p></li></ul><p>Each model is smarter and more intelligent than the previous. But this also means it uses more tokens and thus is more expensive to use.</p><p>Among various AI coding assistants, Claude Code proved particularly well-suited for this migration:</p><ul><li><p><strong>Long Context Window:</strong> Claude Code can maintain context across thousands of lines of code, understanding relationships between classes and interfaces across multiple files.</p></li><li><p><strong>Tool Integration:</strong> Direct integration with file operations, git commands, and the ability to read, write, and edit files means the entire workflow&#8212;from reading Java files to committing Kotlin conversions&#8212;happens seamlessly.</p></li><li><p><strong>Multi-Step Reasoning:</strong> Each migration required multiple steps: analyzing the Java code, understanding its purpose, applying appropriate Kotlin patterns, preserving annotations, and verifying correctness. Claude Code excels at this type of complex, multi-step reasoning.</p></li><li><p><strong>Framework Knowledge:</strong> Claude Code demonstrated deep understanding of Spring Framework, Hibernate/JPA, and other technologies used in the codebase, correctly preserving critical annotations and architectural patterns.</p></li><li><p><strong>Incremental Progress:</strong> The ability to work in batches, commit incrementally, and maintain a clear audit trail through git history was essential for a migration of this scale.</p></li></ul><h2>Migration Layer by Layer</h2><p>Let&#8217;s dive into the specific transformations applied to each architectural layer of the application, examining real examples from our migration and explaining the Kotlin idioms that make the code more expressive and maintainable.</p><h3>Domain Entities: The Foundation</h3><p>Domain entities are the core data structures representing concepts like Patients, Observations, Concepts, and Locations. These classes are heavily annotated with JPA/Hibernate mappings and form the foundation of the entire application.</p><p><strong>Example: </strong><code>LocationTag</code><strong> Entity</strong></p><p><strong>Java Version</strong> (65 lines):</p><pre><code><code>@Entity 
@Table(name = "location_tag") 
@Audited 
@AttributeOverride(name = "name", column = @Column(name = "name", nullable = false, length = 50)) 
public class LocationTag extends BaseChangeableOpenmrsMetadata { 
 
    private static final long serialVersionUID = 7654L; 
    private Integer locationTagId; 
    
    public LocationTag() { 
    } 
 
    public LocationTag(Integer locationTagId) { 
        this.locationTagId = locationTagId; 
    } 
 
    public LocationTag(String name, String description) { 
        setName(name); 
        setDescription(description); 
    } 
 
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    @Column(name = "location_tag_id", nullable = false) 
    public Integer getLocationTagId() { 
        return this.locationTagId; 
    } 
 
    public void setLocationTagId(Integer locationTagId) { 
        this.locationTagId = locationTagId; 
    } 
 
    @Override 
    public Integer getId() { 
        return getLocationTagId(); 
    } 
 
    @Override 
    public void setId(Integer id) { 
        setLocationTagId(id); 
    } 
 
    @Override 
    public String toString() { 
        return getName() != null ? getName() : ""; 
    } 
} </code></code></pre><p><strong>Kotlin Version</strong> (65 lines &#8594; 40 lines, 38% reduction):</p><pre><code><code>@Audited 
@Entity 
@Table(name = "location_tag") 
@AttributeOverride(name = "name", column = Column(name = "name", nullable = false, length = 50)) 
class LocationTag() : BaseChangeableOpenmrsMetadata() { 
 
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    @Column(name = "location_tag_id", nullable = false) 
    var locationTagId: Int? = null 
 
    constructor(locationTagId: Int?) : this() { 
        this.locationTagId = locationTagId 
    } 
 
    constructor(name: String?, description: String?) : this() { 
        this.name = name 
        this.description = description 
    } 
 
    override fun toString(): String = name ?: "" 
 
    override fun getId(): Int? = locationTagId 
 
    override fun setId(id: Int?) { 
        locationTagId = id 
    } 
 
    companion object { 
        private const val serialVersionUID = 7654L 
    } 
} </code></code></pre><p><strong>Key Kotlin Idioms Applied:</strong></p><ul><li><p><strong>Properties Replace Getters/Setters:</strong> The <code>locationTagId</code> field becomes a property with automatic getter/setter generation. JPA annotations are applied directly to the property.</p></li><li><p><strong>Nullable Types:</strong> <code>Int?</code> explicitly declares that the ID can be null, making null-safety part of the type system rather than relying on documentation or runtime checks.</p></li><li><p><strong>Companion Objects:</strong> The <code>serialVersionUID</code> moves to a companion object, Kotlin&#8217;s answer to static members. The <code>const val</code> modifier ensures it&#8217;s compiled as a compile-time constant.</p></li><li><p><strong>Elvis Operator:</strong> <code>name ?: ""</code> replaces the verbose ternary operator, providing a concise way to handle null values.</p></li><li><p><strong>Expression Bodies:</strong> Methods with single expressions can be written inline with <code>=</code>, improving readability.</p></li><li><p><strong>Secondary Constructors:</strong> Kotlin&#8217;s secondary constructor syntax is more compact while maintaining all the functionality of the Java version.</p></li></ul><h3>Service Interfaces: Contracts and Boundaries</h3><p>Service interfaces define the contract between the presentation layer and business logic. These are crucial for Spring&#8217;s dependency injection and testing.</p><p><strong>Example: </strong><code>ConceptService</code><strong> Interface</strong></p><p><strong>Java Version</strong> (excerpt):</p><pre><code><code>@Transactional 
public interface ConceptService extends OpenmrsService { 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    Concept getConcept(Integer conceptId) throws APIException; 
 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    List&lt;Concept&gt; getAllConcepts(String sortBy, boolean asc, boolean includeRetired) 
        throws APIException; 
 
    @Authorized(OpenmrsConstants.PRIV_MANAGE_CONCEPTS) 
    Concept saveConcept(Concept concept) throws APIException; 
 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    List&lt;ConceptSearchResult&gt; getConcepts( 
        String phrase, 
        List&lt;Locale&gt; locales, 
        boolean includeRetired, 
        List&lt;ConceptClass&gt; requireClasses, 
        List&lt;ConceptClass&gt; excludeClasses, 
        List&lt;ConceptDatatype&gt; requireDatatypes, 
        List&lt;ConceptDatatype&gt; excludeDatatypes, 
        Concept answersToConcept, 
        Integer start, 
        Integer size 
    ) throws APIException; 
} </code></code></pre><p><strong>Kotlin Version:</strong></p><pre><code><code>@Transactional 
interface ConceptService : OpenmrsService { 
 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    @Throws(APIException::class) 
    fun getConcept(conceptId: Int?): Concept? 
 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    @Throws(APIException::class) 
    fun getAllConcepts(sortBy: String?, asc: Boolean, includeRetired: Boolean): List&lt;Concept&gt; 
 
    @Authorized(OpenmrsConstants.PRIV_MANAGE_CONCEPTS) 
    @Throws(APIException::class) 
    fun saveConcept(concept: Concept): Concept 
 
    @Transactional(readOnly = true) 
    @Authorized(OpenmrsConstants.PRIV_VIEW_CONCEPTS) 
    @Throws(APIException::class) 
    fun getConcepts( 
        phrase: String?, 
        locales: List&lt;Locale&gt;?, 
        includeRetired: Boolean, 
        requireClasses: List&lt;ConceptClass&gt;?, 
        excludeClasses: List&lt;ConceptClass&gt;?, 
        requireDatatypes: List&lt;ConceptDatatype&gt;?, 
        excludeDatatypes: List&lt;ConceptDatatype&gt;?, 
        answersToConcept: Concept?, 
        start: Int?, 
        size: Int? 
    ): List&lt;ConceptSearchResult&gt; 
} </code></code></pre><p><strong>Key Kotlin Idioms Applied:</strong></p><ul><li><p><strong>Function Declaration:</strong> <code>fun</code> keyword with parameter name followed by type (<code>conceptId: Int?</code>) is more natural to read than Java&#8217;s type-before-name convention.</p></li><li><p><strong>Explicit Nullability:</strong> Every parameter and return type explicitly declares whether it can be null. <code>Int?</code> means nullable integer, <code>Int</code> means non-null. This eliminates entire classes of bugs.</p></li><li><p><strong>@Throws Annotation:</strong> Kotlin doesn&#8217;t have checked exceptions, so <code>@Throws</code> documents which exceptions might be thrown for Java interoperability.</p></li><li><p><strong>Immutable Collections by Default:</strong> <code>List&lt;Concept&gt;</code> in Kotlin is read-only by default, preventing accidental modification.</p></li><li><p><strong>Named Parameters:</strong> Kotlin supports named parameters at call sites, making methods with many parameters more readable: <code>getConcepts(phrase = "diabetes", locales = listOf(Locale.ENGLISH), includeRetired = false, ...)</code>.</p></li></ul><h3>Service Implementations: Business Logic</h3><p>Service implementations contain the business logic and orchestrate data access. These classes often have complex dependencies and transaction management.</p><p><strong>Example: </strong><code>MedicationDispenseServiceImpl</code></p><p><strong>Java Version</strong> (108 lines, excerpt):</p><pre><code><code>@Service("medicationDispenseService") 
@Transactional 
public class MedicationDispenseServiceImpl extends BaseOpenmrsService 
    implements MedicationDispenseService { 
 
    private MedicationDispenseDAO dao; 
 
    @Autowired 
    public void setMedicationDispenseDAO(MedicationDispenseDAO dao) { 
        this.dao = dao; 
    } 
 
    public MedicationDispenseDAO getDao() { 
        return dao; 
    } 
 
    @Override 
    @Transactional(readOnly = true) 
    public MedicationDispense getMedicationDispense(Integer id) { 
        return dao.getMedicationDispense(id); 
    } 
 
    @Override 
    public MedicationDispense saveMedicationDispense(MedicationDispense dispense) { 
        if (dispense == null) { 
            throw new IllegalArgumentException("MedicationDispense cannot be null"); 
        } 
        return dao.saveMedicationDispense(dispense); 
    } 
 
    @Override 
    @Transactional(readOnly = true) 
    public List&lt;MedicationDispense&gt; getMedicationDispenses( 
        Patient patient, 
        Drug drug, 
        DateRangeParam dateRangeParam, 
        Integer startIndex, 
        Integer limit 
    ) { 
        if (patient == null) { 
            throw new IllegalArgumentException("Patient cannot be null"); 
        } 
        return dao.getMedicationDispenses(patient, drug, dateRangeParam, startIndex, limit); 
    } 
} </code></code></pre><p><strong>Kotlin Version</strong> (83 lines, 23% reduction):</p><pre><code><code>@Service("medicationDispenseService") 
@Transactional 
open class MedicationDispenseServiceImpl : BaseOpenmrsService(), MedicationDispenseService { 
 
    @Autowired 
    lateinit var dao: MedicationDispenseDAO 
 
    @Transactional(readOnly = true) 
    override fun getMedicationDispense(id: Int?): MedicationDispense? = 
        dao.getMedicationDispense(id) 
 
    override fun saveMedicationDispense(dispense: MedicationDispense): MedicationDispense { 
        require(dispense != null) { "MedicationDispense cannot be null" } 
        return dao.saveMedicationDispense(dispense) 
    } 
 
    @Transactional(readOnly = true) 
    override fun getMedicationDispenses( 
        patient: Patient, 
        drug: Drug?, 
        dateRangeParam: DateRangeParam?, 
        startIndex: Int?, 
        limit: Int? 
    ): List&lt;MedicationDispense&gt; { 
        require(patient != null) { "Patient cannot be null" } 
        return dao.getMedicationDispenses(patient, drug, dateRangeParam, startIndex, limit) 
    } 
} </code></code></pre><p><strong>Key Kotlin Idioms Applied:</strong></p><ul><li><p><strong>Open Class:</strong> The <code>open</code> keyword is crucial for Spring&#8212;it allows CGLIB to create proxies for transaction management. Without it, Spring&#8217;s proxy-based AOP won&#8217;t work properly.</p></li><li><p><strong>lateinit var:</strong> Perfect for dependency injection. The property is non-nullable but initialized after construction. Accessing it before initialization throws a clear exception.</p></li><li><p><strong>Property-Based Injection:</strong> No need for getter/setter&#8212;<code>@Autowired</code> can be applied directly to the property. This eliminates 4 lines of boilerplate per dependency.</p></li><li><p><strong>require() Function:</strong> Kotlin&#8217;s built-in validation function. It throws <code>IllegalArgumentException</code> with a clear message if the condition is false, replacing verbose if-throw blocks.</p></li><li><p><strong>Single-Expression Functions:</strong> Simple delegation methods can be written inline with <code>=</code>, improving readability dramatically.</p></li><li><p><strong>String Templates:</strong> The error message in <code>require</code> uses a lambda, allowing for lazy evaluation and automatic string conversion.</p></li></ul><h3>Validators: Data Integrity</h3><p>Validators ensure data integrity before persistence. They implement Spring&#8217;s <code>Validator</code> interface and contain complex validation logic.</p><p><strong>Example: </strong><code>EncounterRoleValidator</code></p><p><strong>Java Version</strong> (51 lines):</p><pre><code><code>@Component 
@Handler(supports = {EncounterRole.class}) 
public class EncounterRoleValidator extends RequireNameValidator { 
 
    @Autowired 
    private EncounterService encounterService; 
 
    @Override 
    public void validate(Object obj, Errors errors) { 
        super.validate(obj, errors); 
        if (errors.hasErrors()) { 
            return; 
        } 
 
        EncounterRole encounterRole = (EncounterRole) obj; 
        String name = encounterRole.getName(); 
        if (name != null) { 
            name = name.trim(); 
        } 
 
        if (StringUtils.hasText(name)) { 
            EncounterRole duplicate = encounterService.getEncounterRoleByName(name); 
            if (duplicate != null) { 
                if (!OpenmrsUtil.nullSafeEquals(encounterRole.getUuid(), duplicate.getUuid())) { 
                    errors.rejectValue("name", "EncounterRole.duplicate.name", 
                        "Specified Encounter Role name already exists"); 
                } 
            } 
        } 
    } 
} </code></code></pre><p><strong>Kotlin Version</strong> (51 lines, same length but more readable):</p><pre><code><code>@Component 
@Handler(supports = [EncounterRole::class]) 
class EncounterRoleValidator : RequireNameValidator() { 
 
    @Autowired 
    private lateinit var encounterService: EncounterService 
 
    override fun validate(obj: Any, errors: Errors) { 
        super.validate(obj, errors) 
        if (errors.hasErrors()) return 
 
        val encounterRole = obj as EncounterRole 
        val name = encounterRole.name?.trim() 
 
        if (!name.isNullOrBlank()) { 
            val duplicate = encounterService.getEncounterRoleByName(name) 
            if (duplicate != null &amp;&amp; encounterRole.uuid != duplicate.uuid) { 
                errors.rejectValue( 
                    "name", 
                    "EncounterRole.duplicate.name", 
                    "Specified Encounter Role name already exists" 
                ) 
            } 
        } 
    } 
} </code></code></pre><p><strong>Key Kotlin Idioms Applied:</strong></p><ul><li><p><strong>Array Syntax for Annotations:</strong> <code>[EncounterRole::class]</code> is more concise than <code>{EncounterRole.class}</code>.</p></li><li><p><strong>Smart Casts:</strong> After <code>obj as EncounterRole</code>, the compiler knows <code>obj</code> is an <code>EncounterRole</code> and allows direct property access without repeated casting.</p></li><li><p><strong>Safe Call Operator:</strong> <code>encounterRole.name?.trim()</code> safely handles null values&#8212;if name is null, the entire expression evaluates to null rather than throwing NPE.</p></li><li><p><strong>isNullOrBlank():</strong> Kotlin&#8217;s extension function combines null check and blank check in one readable operation, replacing <code>StringUtils.hasText()</code>.</p></li><li><p><strong>Property Access:</strong> <code>encounterRole.uuid</code> instead of <code>encounterRole.getUuid()</code> is more concise and reads like natural language.</p></li><li><p><strong>Simplified Conditionals:</strong> The combined null check and UUID comparison is more readable without the verbose <code>OpenmrsUtil.nullSafeEquals()</code>.</p></li></ul><h3>Data Access Objects: Persistence Layer</h3><p>DAOs handle database interactions using Hibernate. These range from simple interfaces to complex query builders.</p><p><strong>Example: </strong><code>OpenmrsRevisionEntity</code></p><p><strong>Java Version</strong> (48 lines):</p><pre><code><code>@Entity 
@RevisionEntity(OpenmrsRevisionEntityListener.class) 
@Table(name = "liquibasechangelog") 
public class OpenmrsRevisionEntity implements Serializable { 
 
    private static final long serialVersionUID = 1L; 
    
    @Id 
    @RevisionNumber 
    @Column(name = "id") 
    private int id; 
 
    @RevisionTimestamp 
    @Column(name = "dateexecuted") 
    private long timestamp; 
 
    @Column(name = "author") 
    private String author; 
 
    public int getId() { 
        return id; 
    } 
 
    public void setId(int id) { 
        this.id = id; 
    } 
 
    public long getTimestamp() { 
        return timestamp; 
    } 
 
    public void setTimestamp(long timestamp) { 
        this.timestamp = timestamp; 
    } 
 
    public String getAuthor() { 
        return author; 
    } 
 
    public void setAuthor(String author) { 
        this.author = author; 
    } 
} </code></code></pre><p><strong>Kotlin Version</strong> (35 lines, 27% reduction):</p><pre><code><code>@Entity 
@RevisionEntity(OpenmrsRevisionEntityListener::class) 
@Table(name = "liquibasechangelog") 
open class OpenmrsRevisionEntity : Serializable { 
 
    @Id 
    @RevisionNumber 
    @Column(name = "id") 
    var id: Int = 0 
 
    @RevisionTimestamp 
    @Column(name = "dateexecuted") 
    var timestamp: Long = 0 
 
    @Column(name = "author") 
    var author: String? = null 
 
    companion object { 
        private const val serialVersionUID = 1L 
    } 
} </code></code></pre><p><strong>Key Kotlin Idioms Applied:</strong></p><ul><li><p><strong>Open Class for Hibernate:</strong> Hibernate creates proxies of entity classes for lazy loading. <code>open</code> allows this proxying to work correctly.</p></li><li><p><strong>Properties with Annotations:</strong> JPA annotations work directly on properties, eliminating all getter/setter boilerplate&#8212;12 lines reduced to 3 property declarations.</p></li><li><p><strong>Default Values:</strong> Properties can have default values (<code>= 0</code>), making the code more explicit about initial state.</p></li><li><p><strong>Nullable vs Non-Nullable:</strong> <code>author: String?</code> is nullable (might not have an author), while <code>id: Int</code> is non-null (always has an ID).</p></li><li><p><strong>Class Reference Syntax:</strong> <code>OpenmrsRevisionEntityListener::class</code> is Kotlin&#8217;s class reference, equivalent to Java&#8217;s <code>.class</code>.</p></li></ul><h2>Migration Statistics and Outcomes</h2><p>Our migration effort covered multiple architectural layers with impressive results:</p><ul><li><p><strong>Entities (167 files):</strong> 43,715 Java lines &#8594; 23,084 Kotlin lines (47% reduction)</p></li><li><p><strong>Service Interfaces (10 files):</strong> Significant boilerplate reduction while maintaining full compatibility</p></li><li><p><strong>Service Implementations (5 files):</strong> 412 lines &#8594; 346 lines (16% reduction)</p></li><li><p><strong>Validators (5 files):</strong> 220 lines &#8594; 208 lines (5% reduction)</p></li><li><p><strong>DAOs (15 files):</strong> ~350 lines &#8594; ~300 lines (14% reduction)</p></li><li><p><strong>Utilities (20 files):</strong> 764 lines &#8594; 675 lines (12% reduction)</p></li></ul><p>Overall, we achieved approximately 20-47% code reduction depending on the layer, with entity classes showing the most dramatic improvements due to property syntax replacing getter/setter boilerplate.</p><h2>Challenges and Solutions</h2><h3>Challenge 1: Spring Framework Compatibility</h3><p><strong>Issue:</strong> Spring uses CGLIB proxies for transaction management and AOP, which require classes to be non-final.</p><p><strong>Solution:</strong> Mark classes as <code>open</code> explicitly. Initially, we converted classes directly, but Spring couldn&#8217;t create proxies. Adding <code>open</code> to service implementations and certain entities solved this immediately.</p><h3>Challenge 2: JPA/Hibernate Annotations</h3><p><strong>Issue:</strong> JPA annotations on getters needed to move to properties, but placement matters.</p><p><strong>Solution:</strong> Annotations go on the property declaration. For field-based access, annotations work directly on the property. For property-based access, we used <code>@field:</code> or <code>@get:</code> site targets when necessary.</p><h3>Challenge 3: Null Safety Migration</h3><p><strong>Issue:</strong> Java code doesn&#8217;t distinguish nullable from non-nullable. Making everything nullable defeats Kotlin&#8217;s purpose; making everything non-null causes crashes.</p><p><strong>Solution:</strong> Analyzed each field contextually. IDs that might not be set before persistence are nullable (<code>Int?</code>). Required business fields are non-null. When in doubt, we started nullable and refined after testing.</p><h3>Challenge 4: Collection Immutability</h3><p><strong>Issue:</strong> Kotlin&#8217;s <code>List</code> is read-only; Java code often expects mutable collections.</p><p><strong>Solution:</strong> Used <code>MutableList</code> for collections that need modification. For return types from repository methods, <code>List</code> (immutable) is preferred unless the caller needs to modify the collection.</p><h3>Challenge 5: Checked Exceptions</h3><p><strong>Issue:</strong> Kotlin doesn&#8217;t have checked exceptions, but Java callers expect them.</p><p><strong>Solution:</strong> Added <code>@Throws</code> annotations to interface methods for Java interoperability, documenting which exceptions might be thrown.</p><h2>Best Practices Learned</h2><ul><li><p><strong>Migrate Layer by Layer:</strong> Start with leaf dependencies (entities) and work up to services. This minimizes compilation issues.</p></li><li><p><strong>Commit Frequently:</strong> Small, focused commits make it easier to identify issues and revert if necessary.</p></li><li><p><strong>Test Between Batches:</strong> Run tests after each batch of 10-15 files to catch issues early.</p></li><li><p><strong>Preserve Annotations Carefully:</strong> Framework annotations are critical&#8212;double-check they&#8217;re preserved correctly.</p></li><li><p><strong>Use IDE Verification:</strong> Even with AI assistance, run IntelliJ&#8217;s &#8220;Analyze Code&#8221; on converted files to catch issues.</p></li><li><p><strong>Document Patterns:</strong> Maintain a pattern guide for the team showing how common Java constructs map to Kotlin idioms.</p></li><li><p><strong>Leverage Companion Objects:</strong> Static members go in companion objects with <code>const val</code> for compile-time constants and <code>@JvmStatic</code> for methods called from Java.</p></li><li><p><strong>Think About Nullability:</strong> Don&#8217;t blindly make everything nullable. Use non-null types when semantically appropriate, and let the compiler help you find bugs.</p></li></ul><h2>Conclusion: The Path Forward</h2><p>Migrating from Java to Kotlin represents more than just a syntax change&#8212;it&#8217;s an evolution in how we think about code safety, expressiveness, and maintainability. The combination of Kotlin&#8217;s powerful features and AI-assisted migration allowed us to modernize a large codebase systematically while maintaining functionality and quality.</p><p>The results speak for themselves: significant code reduction, improved null safety, and more maintainable code. New developers joining the project find Kotlin code more approachable, with less boilerplate to wade through before understanding business logic.</p><p>For teams considering similar migrations:</p><ul><li><p><strong>Start Small:</strong> Migrate utilities and simple classes first to build confidence.</p></li><li><p><strong>Establish Patterns:</strong> Document your Kotlin idioms early and apply them consistently.</p></li><li><p><strong>Leverage AI:</strong> Use AI assistance not just for conversion but for learning idiomatic patterns.</p></li><li><p><strong>Test Thoroughly:</strong> Maintain your test suite and run it frequently.</p></li><li><p><strong>Incremental Progress:</strong> Don&#8217;t aim for perfection&#8212;migrate incrementally and refine over time.</p></li></ul><p>The investment in migration pays dividends in long-term maintainability, developer productivity, and code safety. For OpenMRS, this migration positions the platform for modern development practices while preserving the stability and reliability that healthcare providers depend on.</p><p>As we continue migrating the remaining files, we&#8217;re not just translating code&#8212;we&#8217;re improving it, making it more robust, more readable, and more maintainable for the next decade of development.</p><p>One thing that we&#8217;ve learned from this migration is that, though AI is doing the bulk of the work, we, as humans, stay responsible for the final outcome. So, code reviews are essential, as is knowledge of not just Kotlin itself, but of the idiomatic use of Kotlin. Migration with AI, but with the user in the loop.</p>]]></content:encoded></item><item><title><![CDATA[Part 2: Getting Started with Object-Oriented Programming in Python]]></title><description><![CDATA[Or. Why is OO Programming So Hard? Part II]]></description><link>https://deepengineering.net/p/part-2-getting-started-with-object</link><guid isPermaLink="false">https://deepengineering.net/p/part-2-getting-started-with-object</guid><dc:creator><![CDATA[Steven Lott]]></dc:creator><pubDate>Tue, 10 Mar 2026 14:03:55 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ec55e455-845b-450f-8c15-d1b0e2a4b87c_2752x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://deepengineering.substack.com/p/part-1-getting-started-with-object">Previously</a>, we looked at some of the reasons OO programming is hard. And, we pitched a few strategies for getting started quickly:  </p><ol><li><p>Focus on the data. </p></li><li><p>Start with a Python dataclass. </p></li><li><p>Add attributes and experiment. </p></li><li><p>Review and rework the experiment. </p></li><li><p>Refactor classes to narrow responsibilities.  </p></li></ol><p>We want to look at some alternatives to using the <code>dataclasses</code> module and the <code>@dataclass</code> decorator.  </p><h2>Why avoid a dataclass?</h2><p>Dataclasses are really helpful. As we saw, they define a number of built-in methods.  </p><p>They're not <strong>always</strong> the best choice.  </p><p>Consider the humble playing card.  </p><p>Does it undergo any state changes? Can the Jack of Hearts morph into the Jack of Diamonds? Clearly, that's absurd.  </p><p>It's just as absurd as the number 42 somehow transforming into 41.  </p><p>In Python, numbers (and strings and tuples) are <strong>immutable</strong>. The value &#8212; the internal state &#8212; is fixed.  </p><p>We have two ways to make a playing card class immutable:</p><ul><li><p> Use the <code>@dataclass(frozen=True)</code> decorator instead of the <code>@dataclass</code> decorator.  This is a minor change, and easy to experiment with.</p></li><li><p>Switch from dataclass to <code>NamedTuple</code>.  </p></li></ul><p>Let's look at the <code>NamedTuple</code> type definition.  </p><h2>NamedTuple instead of dataclass</h2><p>The <code>NamedTuple</code> base class is defined in the <code>typing</code> module. Since it's a class &#8212; not a decorator function &#8212; it&#8217;s not used the way the <code>@dataclass</code> decorator is used.  Here&#8217;s an example:</p><pre><code><code>from typing import NamedTuple  

class Card(NamedTuple):
    rank: int
    suit: str

    @property
    def points(self) -&gt; int:
        return 10 if self.rank &gt;= 11 else self.rank

    def __str__(self) -&gt; str:
        named = {1: "A", 11: "J", 12: "Q", 13: "K"}
        rank = named.get(self.rank, str(self.rank))
        return f"{rank:&gt;2s}{self.suit}"  </code></code></pre><p>Note that only two lines of code changed.  </p><ol><li><p><code>from typing import NamedTuple</code>.</p></li><li><p><code>class Card(NamedTuple):</code>.</p></li></ol><p>The rest of the code looks the same.  </p><p>The behavior doesn't change in any big, obvious way. This may, however, expose a bug where some part of a big application tried to change the internal state of a card.</p><p>(And no, we&#8217;re not going to talk much about what a base class is. This is a tutorial on how to get started quickly.)</p><p>In this application domain, the distinction between dataclass and named tuple is minor. </p><p>In other applications, however, the named tuple does something a simple data class can't do.  </p><h2>Sets, dictionary keys, and immutability </h2><p>As we get more comfortable with object-oriented design, we start to look at combining objects. This is particularly easy in Python, where we can have lists of objects without having to do any serious programming. Specifically, the <code>Deck</code> class had a <code>list[Card]</code> attribute. This is a type <em>hint</em>, and is optional. While tools can check it for us, sometimes it&#8217;s a handy reminder of what the intent behind the variable or attribute is.</p><p>There are two other built-in data collections &#8212; sets and dictionaries &#8212; that have tiny caveats surrounding their use.  </p><ul><li><p>Sets can only have immutable objects as members.  </p></li><li><p>Dictionary keys must be immutable objects.  </p></li></ul><p>The rule is a bit more nuanced than that. We'll hold off on the complication for a moment.  </p><p>For card games with a conventional 52-card deck, there are no duplicated cards. We don't have much use for sets or dictionaries that work with ``Card`` objects.  </p><p>To discover if a hand has a pair or three of a card (called a "pair royal" in Cribbage), a dictionary that maps a rank to a count of cards with that rank is handy. In this case, the rank is an integer; they're immutable.  Here&#8217;s some code to build the dictionary of ranks and counts:</p><pre><code>rank_counts: dict[int, int] = {} 
for card in hand.cards:
     if card.rank not in rank_counts:
         rank_counts[card.rank] = 0
     rank_counts[card.rank] += 1    </code></pre><p>We can then check the value of <code>rank_counts.value()</code> to see if there's a number other than one. For example:</p><pre><code>pattern = set(rank_counts.values())</code></pre><p>The patterns will be sets like <code>{1}</code>, <code>{1, 2}</code>, <code>{1, 3}</code>, <code>{1, 4}</code>, <code>{2, 3}</code>. Note that this particular code example can't distinguish one pair from two pair. It's not <em>great</em> for working with Cribbage hands, but it does show of some Python programming techniques.  </p><p>These examples work because the <code>int</code> type is immutable. The suit's a string, and the ``str`` type is also immutable. Want to know if the hand's a flush?</p><pre><code>suits = {card.suit for card in hand.cards}
flush = len(suits) == 1  </code></pre><p>We've created a set of suits. If all the cards are the same suit, the set of distinct suits will have one value.  </p><p>These have been some places where immutability is necessary.  </p><h2>Do we really need immutability?</h2><p>Immutability isn't needed until the application depends an object being a member of a set or  a key to a dictionary. The change from <code>@dataclass class Whatever:</code> to <code>class Whatever(NamedTuple):</code> is minor.  </p><p>As part of strategy 4, <strong>Review and rework the experiment,</strong> this is one of the changes that may be necessary. As part of the overall objective &#8212; avoid staring at a blank screen wondering how to start &#8212; this is something we often need to set aside until we see <code>TypeError: unhashable type: 'Whatever'</code>. </p><p> The error uses the word "unhashable". Above, we said "immutable". Immutable objects are hashable. Other objects can be hashable, including dataclasses created with <code>frozen=True</code>. The distinction seems nuanced, but it&#8217;s not.</p><p>The world of hashable objects includes any class that defines an internal <code>__hash__()</code> method. The tuple and the named tuple classes provide the needed <code>__hash__()</code> method, permitting them to act as dictionary keys and set members. And, a frozen dataclass <strong>also</strong> provides the required method to make it hashable.  </p><h2>Wait, wait, wait</h2><p>At this point, folks with some OO experience &#8212; or folks frustrated by OO tutorials &#8212; will have a question.  </p><p>"All it takes is a method being defined? What about inheritance and delegation and all that?"</p><p>Python's approach is called "Duck Typing".  It&#8217;s based on this:</p><p>"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."      &#8212; James W. Riley  </p><p>If a class has a <code>__hash__()</code> method, that makes any object of that class hashable. Yes. It's that simple.</p><p>And, yes, you can define a <code>__hash__()</code> method for a class that does not have a fixed, immutable state. If the hash method and the equality test methods don&#8217;t agree, problems will ensue. (This is a quick start, rely on the built-in features and things will work nicely.)</p><p>There&#8217;s one other way to collect data, similar to a dataclass and a NamedTuple.</p><h2>Typed dictionary instead of a dataclass </h2><p><em>Typed</em> dictionary?  </p><p>We have two common varieties of dictionaries:  </p><p>-   <strong>Homogenous</strong>. Described by <code>dict[KeyType, ValueType]</code>, where the keys are all of one type and the values are of another type.  Our card-game example used a dictionary with keys that are integer ranks and values that are integer counts of cards with that rank.  </p><p>-   <strong>Heterogenous</strong>. In this case, each key's value might have a distinct type.  </p><p>A heterogenous dictionary &#8212; where the values have different types is helpful for some things. It's not quite so universally cool as a dataclass.  </p><p>The syntax looks nearly identical to the <code>NamedTuple</code> class.  </p><pre><code>from typing import TypedDict

class Card_D(TypedDict):
    rank: int
    suit: str  </code></pre><p>This also fits with strategy 1, <strong>Focus on the data</strong>.  What&#8217;s important is this class defines a specialized kind of dictionary. The initialization and some output will be noticeably different. It's awkward to define methods or properties.  </p><p>Even the initialization can be a little confusing. One choice is to use explicit parameter names:</p><pre><code>&gt;&gt;&gt; c_1 = Card_D(rank=11, suit="&#9825;")
&gt;&gt;&gt; c_1
{'rank': 11, 'suit': '&#9825;'}  </code></pre><p>The parameter names of <code>rank</code> and <code>suit</code>  are required to create a <code>Card_D</code> dictionary. The output looks like a dictionary; we can't <strong>easily</strong> tweak the <code>__str__()</code> method.  (Type-checking tools will warn us away from doing it.)</p><p>The named parameter style is optional to create a tuple or a dataclass. (It's not required because tuple fields have an ordering; that's part of the point of a tuple definition.) We can switch our previous dataclass and NamedTuple examples to use explicit parameter names. This makes it possible to switch between using dictionaries, tuples, and dataclasses. </p><p>Here's the second way to create a typed dict object, starting from another dictionary:  </p><pre><code>&gt;&gt;&gt; c_2 = Card_D({"rank": 11, "suit": "&#9825;"})     
&gt;&gt;&gt; c_2     
{'rank': 11, 'suit': '&#9825;'}</code></pre><p>The <code>Card_D</code> dictionary is built from an existing dictionary. This can be handy when working with JSON or TOML files, or parsing a CSV file.  </p><p>What's important is that tools like <strong>pyright</strong> and <strong>mypy</strong> can check the type hints in the class definition and all the places the class is used. These tools can provide useful feedback. Since this is about getting started without too much design struggle, we won't go further. This is a topic for ongoing study. It's not for a quick start with OO programming.  </p><p>A dictionary is always mutable. Dictionaries can't be collected into sets. A dictionary can't be used as a key into another dictionary.  </p><h2>Similarities and differences  </h2><p>Typed dictionaries are superficially similar to named tuples and dataclasses. All three give us a quick way to provide a class with attribute names and their datatypes.  </p><p>The differences?  </p><ul><li><p>A dataclass has a variety of options. By default it's mutable; the <code>frozen=True</code> parameter makes the objects immutable. Any mutable object can have additional attribute values assigned to it.  Forcing new attributes into an object makes type-hint checking tools like <strong>mypy</strong> very nervous.  </p></li><li><p>A named tuple is immutable.     We can never add new field values. Any code that tries to update this will be rejected by type-hint checking tools and ordinary linting tools.     Also, the fields of a named tuple have a defined ordering, and can be processed by index as well as by name.      The expression <code>some_card[0]</code> is the same as <code>some_card.rank</code>. Using names seems more clear than using an index.  </p></li><li><p>A typed dictionary is a dictionary; we can't easily add methods to it.     (We can, but type-checking tools will complain.)     It's mutable.     The fields can only be referenced using <code>some_dict[key]</code> syntax.     We can trivially add new field values.  </p></li></ul><p>We have a spectrum of features. How do we choose?  </p><p><strong>Strategy 2. Start with a Python dataclass</strong>.</p><p>- Switch to named tuples to get hash values.  </p><p>- Switch to typed dictionaries only if the collection of attributes keeps growing and changing and the open-ended flexibility of a dictionary seems helpful.  </p><p>Above all, remember strategy 4, <strong>Review and rework the experiment</strong>.</p><h2>Revisiting refactoring </h2><p>Strategy 5, <strong>Refactor classes to narrow responsibilities</strong>, has a secret message behind it. There are many design principles for OO programming. One batch of principles is called <strong>SOLID</strong>. Because that&#8217;s a handy acronym to remember them.  </p><p>Of these principles, the <strong>I</strong> principle, "Interface Segregation", is particularly helpful.  </p><p>The idea is to minimize the external interface a class presents to its collaborators. A class should expose only what is necessary; nothing more.  </p><p>In our examples, the <code>Deck</code> class has a sequence of <code>Card</code> objects and that's about all there is to this class. Pretty slim. The class avoids any mention of a game, the players, the <code>Hand</code> collection, or anything outside the very narrow space of creating a deck of <code>Card</code> objects and shuffling.  </p><p>Related to this is the <strong>S</strong> principle, "Single Responsibility", which is similar in many respects. Some people find it easier to think about the responsibilities of a class than to think about the interface to a class. The interface views the class from the outside, the way collaborators see it. The responsibilities describe the class on the inside, with a focus on the underlying purpose behind the methods and attributes.  </p><p>The <strong>L</strong> and <strong>O</strong> principles, "Liskov Substitution" and "Open/Closed", apply to inheritance. As long as the specialized version has the same interface as the generic version, it follows the Liskov Substitution principle. Since a specialized version can be extended, it's open to extension and closed to modification. This post is a general message about getting started quickly. A more specialized message, extending and adding features to this, will be a separate post.  </p><p>The <strong>D</strong> principle, "Dependency Injection" &#8212; sometimes called "Dependency Inversion" &#8212; is central to these examples.  </p><h2>Dependency questions  </h2><p>One bit of unpleasantness about the various ``Deck`` implementations is the reference to ``Card`` or ``Card_NT`` or ``Card_D`` or whatever variant on the ``Card`` class we have in mind.  </p><pre><code>@dataclass     
class Deck_D:     
    cards: list[Card_D]      

    def __init__(self) -&gt; None:     
        self.cards = [
            Card_D(rank=rank, suit=suit)
            for rank in range(1, 14)
                 for suit in "&#9827;&#9826;&#9825;&#9824;"
        ]
        random.shuffle(self.cards)  </code></pre><p>What we don't like is the <code>Card_D(rank=rank, suit=suit)</code> part of this. This <code>Deck_D</code> class depends on another <code>Card_D</code> class in a way that's a potential pain to change.  </p><p>Sure. We <strong>can</strong> edit the file. But. Is this line of code the <strong>only</strong> place we have to make the change? What if we miss some changes? (Hint: It may explode into a debugging nightmare.)  </p><p>We want to replace a direct dependency with something indirect. We want to replace a very specific "call 555-123-5839" with an indirect "call your mother." The value of mother can be replaced with distinct numbers. We want to inject the class reference when the application runs, so we don't have to edit the code carefully to be sure we've changed <strong>**everything**</strong> correctly and consistently. (We want to make it more like changing a configuration file, an environment variable, a command-line parameter than changing the code.)  </p><p>In Python, we can <strong>**always**</strong> assign an object to another variable. The type hints can get confusing, so we'll drop a few of them in the following example: </p><pre><code>from dataclasses import dataclass     
import random     
from typing import ClassVar      

@dataclass     
class Deck:         
    card_class: ClassVar[type] = Card         
    cards: list          

    def __init__(self) -&gt; None:             
        self.cards = [                 
            Deck.card_class(rank=rank, suit=suit)                 
            for rank in range(1, 14)                 
                for suit in "&#9827;&#9826;&#9825;&#9824;"             
        ]
        random.shuffle(self.cards)  </code></pre><p>We've introduced a <code>ClassVar</code>. This is an attribute that's part of the class, not part of each object created by the class. It's shared. It's accessed via <code>self.card_class</code>. It can also be accessed via <code>Deck.card_class</code>, which makes it more clear.  </p><p>Also, we dropped the <code>list[Card]</code> hint, using only <code>list</code>. Why? The class name <code>Card</code> was too specific. We want to use the same type that the <code>card_class</code> uses. We'll get to this below.  </p><p>Note that we do not have to do anything magical or mysterious to refer to a class. It's another object; an object used to create objects. (Unlike some languages where there are complicated-looking <code>*</code> and <code>&amp;</code> operators involved.)  </p><p>The worrying <code>Card_D</code> class name is replaced with <code>Deck.card_class</code>. This will create an instance of the class provided as the value for the <code>card_class</code> attribute. The <code>Deck</code> class is tiny, so this is all there is. The idea is that a much more complicated class might have multiple references to ``Card``, we need <strong>**all**</strong> those references to be consistent. We need each class to encapsulate the responsibility for managing their collaborative relationships.  </p><h2>Generic relationships  </h2><p>The essence of dependency injection is to use generic type names in the definition of a class. We can then replace the generic type with a concrete type to create something useful at run time. In this case, <code>card_class: ClassVar[type] = Card</code> is hopelessly vague. It uses the <code>type</code> type. This means any type. We could use <code>int</code> or <code>str</code>.  This seems unhelpful. We know types like those can't work; we&#8217;d like to provide a hint that lets a type-hint-checker warn us of potential flaws.  </p><p>To get useful warnings, we should really have something a tiny bit more specific than type. Something with a hint about the two initialization parameters.  </p><p>Since all of our variant <code>Card</code> implementations are more-or-less identical, we can borrow the Duck Typing philosophy and specify the minimum features &#8212; the walk, swim, quack &#8212; of an acceptable class. In Python parlance, this is a <em>protocol</em>.  </p><p>What does a <code>Card</code> need? It needs a suit, a rank, and a number of points. It has to look like this:  </p><pre><code>from typing import Protocol      

class CardType(Protocol):     
    rank: int     
    suit: str     
    points: int      

    def __init__(self, rank: int, suit: str) -&gt; None: ...  </code></pre><p>Note the minimalism here: <code>points</code> must be an <code>int</code>. We didn't say whether it's an attribute or a property. This is because we don't much care how it gets done, as long as everyone agrees that it will be of <code>int</code> type.  </p><p>Also note that we provided the <code>__init__()</code> method. This is an automatically generated part of a dataclass or a named tuple. When we're first starting out, we tried to ignore this. When defining a <code>Protocol</code>, we're not allowed to assume things that may be done automatically. We can't assume it because someone would write the whole class from scratch, avoiding any of the built-in helpers. If we state the features a collaborator must find, tools like <strong>pyright</strong> and <strong>mypy</strong> can spot potential problems.  </p><p>Finally, note that we didn't write out the implementation of <code>__init__()</code>. This is a protocol. We provided the signature and left the details to be <code>...</code>. (And yes, <code>...</code> is valid Python syntax; it's not a cheat code used for blog postings.)  </p><p>To fling breadcrumbs to the duck analogy: if we don't specify everything, we may see an animal that doesn't swim like a duck, and is actually a coot.  </p><p>We can then refactor <code>Deck</code> slightly to name the protocol instead of naming one (and only one) specific class.  </p><pre><code>@dataclass     
class Deck:     
    card_class: ClassVar[type[CardType]]     
    cards: list[CardType]      

    def __init__(self) -&gt; None:         
        self.cards = [         
            self.card_class(rank=rank, suit=suit)        
            for rank in range(1, 14)         
                for suit in "&#9827;&#9826;&#9825;&#9824;"       
        ]
        random.shuffle(self.cards)  </code></pre><p>We've replaced a class with a protocol name. The <code>card_class</code> variable doesn't have an initial value, either.  </p><p>We can use this generic deck with examples like the following:  </p><pre><code>&gt;&gt;&gt; from cards_3 import Card, Deck     
&gt;&gt;&gt; class CardDeck(Deck):     
...     card_class = Card     
&gt;&gt;&gt; d = CardDeck()</code></pre><p>This example creates a specialization of <code>Deck</code>, called <code>CardDeck</code>. Every instance of this specialized class will have <code>card_class</code> set to <code>Card</code>. This states, emphatically, what type of <code>Card</code> this class collaborates with.  </p><p>Anything with the <code>CardType</code> protocol is acceptable. Other types will not be acceptable.  </p><p>Looking back at the <code>Deck</code> class, this class name appears more than once. We can be <strong>sure</strong> they will always be consistent when there's a change. And tools like <strong>pyright</strong> and <strong>mypy</strong> can confirm that these types are used consistently.  </p><h2>What's next?  </h2><p>We've got two classes working nicely. We can switch among various <code>Card</code> variants.  We can move on to refactoring the <code>Hand</code> class to be generic, also.  </p><p>Then.  </p><p>We can start looking more closely at the original question. We embarked on this to settle an argument over best ways to play Cribbage. We have a foundation on which we can build a useful simulation.  </p><p>Perhaps the most important aspect of this is flexibility. We've made <strong>numerous</strong> changes just trying to get started.  </p><p>This is the overall summary of the 5 strategies:  </p><p>Don't waste time on <strong>Look Before You Leap</strong>. (LBYL)  </p><p>Or, viewed another way.  </p><p><strong>It's Easier to Ask Forgiveness Than Permission</strong>. (EAFP.)  </p><p>Python permits EAFP programming. Don't stare at the blank screen; start creating dataclasses and seeing if the interaction is useful and produces the results you need.</p><div><hr></div><p><strong>About the Author:</strong></p><p><strong>Steven Lott</strong> has been programming since computers were large, expensive, and rare. Working for decades in high tech has given him exposure to a lot of ideas and techniques, some bad, but most are helpful to others. Since the 1990s, Steven has been engaged with Python, crafting an array of indispensable tools and applications. His profound expertise has led him to contribute significantly to Packt Publishing, penning notable titles like <a href="https://www.packtpub.com/en-us/product/mastering-object-oriented-python-9781789531404">Mastering Object-Oriented Python</a> (Packt, 2019), <a href="https://www.packtpub.com/en-us/product/modern-python-cookbook-9781835466384">Modern Python Cookbook</a> (Packt, 2024), and <a href="https://www.packtpub.com/en-us/product/python-real-world-projects-9781803246765">Python Real-World Projects</a> (Packt, 2023). A self-proclaimed technomad, Steven&#8217;s unconventional lifestyle sees him residing on a boat, often anchored along the vibrant east coast of the US. He tries to live by the words &#8220;Don&#8217;t come home until you have a story.&#8221;</p><div><hr></div><p>Lott is currently working on the 5th edition of Python Object-Oriented Programming, due later this year. If you liked what you read in this article and would like to read more while you wait for the new edition, you can check out <a href="BOOK_LINK">Python Object-Oriented Programming, 4th Ed. (Packt, 2021)</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://www.packtpub.com/en-us/product/python-object-oriented-programming-9781801075237" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S24_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 424w, https://substackcdn.com/image/fetch/$s_!S24_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 848w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1272w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000" width="340" height="419.75308641975306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0059565-ab59-4421-90dd-de189e63939f_810x1000&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1000,&quot;width&quot;:810,&quot;resizeWidth&quot;:340,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Object-Oriented Programming&quot;,&quot;title&quot;:&quot;Python Object-Oriented Programming&quot;,&quot;type&quot;:null,&quot;href&quot;:&quot;https://www.packtpub.com/en-us/product/python-object-oriented-programming-9781801075237&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Object-Oriented Programming" title="Python Object-Oriented Programming" srcset="https://substackcdn.com/image/fetch/$s_!S24_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 424w, https://substackcdn.com/image/fetch/$s_!S24_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 848w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1272w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here is what some readers have said:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B_ZW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 424w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 848w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1272w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png" width="1078" height="432" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:432,&quot;width&quot;:1078,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95317,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 424w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 848w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1272w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"></figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QhcA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QhcA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 424w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 848w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1272w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png" width="1097" height="495" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:495,&quot;width&quot;:1097,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:120161,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!QhcA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 424w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 848w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1272w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bMcY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bMcY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 424w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 848w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1272w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png" width="1070" height="365" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:365,&quot;width&quot;:1070,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:81939,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!bMcY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 424w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 848w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1272w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Part 1: Getting Started with Object-Oriented Programming in Python]]></title><description><![CDATA[It's daunting. There's a lot to learn. We have sympathy. And some strategies.]]></description><link>https://deepengineering.net/p/part-1-getting-started-with-object</link><guid isPermaLink="false">https://deepengineering.net/p/part-1-getting-started-with-object</guid><dc:creator><![CDATA[Steven Lott]]></dc:creator><pubDate>Wed, 27 Aug 2025 07:38:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/70d33dca-aec7-4a44-b3bd-d2f891ac29f2_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is Part 1 of 2. In Part 2, we&#8217;ll look at immutability, hashability, and dependency management: when to switch from <code>@dataclass</code> to <code>NamedTuple</code> or <code>frozen=True</code>, how <code>TypedDict</code> fits a schema-first workflow, and how Protocols enable painless dependency injection.</p><h2><strong>Why is OO Programming So Hard?</strong></h2><p>OO programming is filled with new terms: inheritance, composition, delegation, encapsulation, attribute, method, and other stuff that look like it came from a fake-word generator. There has to be an easier way, right?</p><p>Well.</p><p>Maybe pause a moment.</p><p>Programming is actually difficult. One cause is the way programming spans so many orders of magnitude. From individual bits to megabytes and terabytes. From clock cycles measured in hundreds of nanoseconds to software services that are expected to run flawlessly for years. One of the reason for so many new terms is because there are so many patterns for organizing software. It&#8217;s common practice to borrow words and apply them to software design, exacerbating the level of confusion.</p><p>Euclid may have told Ptolemy there was no &#8220;royal road&#8221; to learning geometry. This is every bit as true of creating software.</p><p>There&#8217;s no shortcut, but there are some ways to get started that help create useful solutions without burning quite so many brain calories on language details and patterns for designing software.</p><h3><strong>What&#8217;s Most Important?</strong></h3><p>The very first steps of a project often amount to staring at a blank screen. For a lot of folks, the tutorials all made perfect sense. But, as they consider the actual problem they want to solve, it starts to feel like the tutorials skipped over some kind of &#8220;how do I start thinking about this?&#8221; step.</p><p>Here&#8217;s a strategy we often use:</p><p><strong>Strategy 1. Focus on the data.</strong></p><p>Some folks hear this and ask, &#8220;Data first? What about user interactions?&#8221;</p><p>Consider the work required to switch from some old application to a shiny, new application. The data is what we move into the new application. The &#8220;user experience&#8221; of screens, buttons, scripts, and commands aren&#8217;t as precious as the information in files and databases.</p><p>It can help to start with quick notes gathered outside the world of application software development. Any old random editor is fine for collecting a summary of what the information sources are and what that information will be used for.</p><p>Python modules start with a docstring. It&#8217;s usually a big triple-quoted string. I start my files with</p><pre><code><code>"""
Some notes.
"""</code></code></pre><p>It&#8217;s easy to jot down anything and everything inside these triple-quoted strings. Some of us draw pictures: they let us annotate lists of data elements with boxes and arrows. There are a lot of possible diagramming techniques. Formalisms don&#8217;t matter as much as capturing a few details to help get started.</p><p>Python is very flexible. Detailed blueprints and gloriously sophisticated UML diagrams aren&#8217;t required. It&#8217;s often harder to start with diagrams than it is to start with code.</p><p>So, let&#8217;s get to some code as quickly as we can.</p><h3><strong>Start with Class Definitions</strong></h3><p>There are several choices for easy-to-write class definitions. A good first choice is to use a <strong>dataclass</strong>.</p><p><strong>Strategy 2. Start with a Python dataclass.</strong></p><blockquote><p>Before you worry about patterns, name things and move data around. We&#8217;ll use <code>@dataclass</code> here because it gets you moving quickly. In <strong>Part 2</strong>, we&#8217;ll keep the same example and show when <code>@dataclass</code> stops pulling its weight&#8212;and what to swap in.</p></blockquote><p>The idea is to put <em>something</em> into the code editor to avoid staring at a blank screen for too long.</p><p>Start with this:</p><pre><code><code>from dataclasses import dataclass

@dataclass
class MyFirstIdea:
    ...  </code></code></pre><p>From this, it&#8217;s possible to explore ideas. Note that this kind of thing is also likely to get deleted when better ideas come along. This is, after all, a blog post on how to get started.</p><p>The class names should be Capitalized, and <code>WordsRunTogether</code>. This is sometimes called <code>SnakeCase</code> because the word has a serpentine outline.</p><h3><strong>Add Attributes</strong></h3><p>The next step is to fill in the attributes of each object. These are the individual fields that define the state of the object. These are details of the objects in the problem domain.</p><p>This isn&#8217;t actually easy. It&#8217;s common to make mistakes. What important is the investment is tiny, so reworking a mistake is easy.</p><p><strong>Strategy 3. Add attributes and experiment.</strong></p><p>The format for the data elements is <code>name: type_hint</code>. Python has a lot of built-in types: <code>bool</code>, <code>str</code>, <code>int</code>, <code>float</code>, <code>complex</code> to name a few. Some types are composed of others: <code>list[</code><em>T</em><code>]</code>, <code>set[</code><em>K</em><code>]</code>, <code>dict[</code><em>K</em><code>, </code><em>T</em><code>]</code> are three examples. Use any type for <em>T</em>. A list of numbers might be <code>list[float]</code> or <code>list[int]</code>, depending on which kind of number&#8217;s involved. The set and dictionary keys have some limitations that we&#8217;ll avoid for a moment.</p><p>Names for attributes are generally <code>lower_case</code>, and use <code>words_with_underscores</code>. This isn&#8217;t a law. It&#8217;s merely a common practice.</p><p>Let&#8217;s say we&#8217;re trying to settle an argument over best ways to play Cribbage. We need to do some simulations. That means we need cards, and decks, and hands.</p><pre><code><code>from dataclasses import dataclass

@dataclass
class Card:
    rank: int
    suit: str
    points: int</code></code></pre><p>This seems to capture the essence of a card. What about a deck of cards? That&#8217;s 52 cards.</p><pre><code><code>@dataclass
class Deck:
    cards: list[Card]</code></code></pre><p>That&#8217;s a start.</p><p>We haven&#8217;t done much, but, we have some elements that let us create working code.</p><h3><strong>Work With The Objects</strong></h3><p>We&#8217;ll start with simple interactions with objects. It&#8217;s important to avoid stumbling over too many details of methods and how methods should be understood and defined.</p><p>A data class will have a handful of methods defined for us. One of the most important is the <code>__init__()</code> method which initializes the object&#8217;s state. It&#8217;s often handy to have this written for us. (As we&#8217;ll see later, it&#8217;s not always ideal, but it&#8217;s a good start.)</p><p>I often write little functions with unhelpful names like <code>demo_1()</code> or <code>make_cards()</code> to make sure the class really is useful.</p><p>We can add something like this to the file we&#8217;re working on:</p><pre><code><code>def make_cards():
    for rank in range(1, 14):
        for suit in "&#9827;&#9826;&#9825;&#9824;":
            c = Card(rank, suit, rank if rank &lt; 11 else 10)
            print(c)

make_cards()</code></code></pre><p>This function creates 52 <code>Card</code> objects, printing them as they&#8217;re built.</p><p>(No, the <code>Card</code> objects not preserved anywhere. We&#8217;ll get to that when we design the <code>Deck</code> class. Each time we create a new <code>Card</code>, we put the variable name <code>c</code> on the object. This means any previous object with the name <code>c</code> isn&#8217;t being used anymore and the storage can be recovered. Garbage collection is automatic.)</p><p>The Unicode suit characters are sometimes tricky to find on most keyboards. Instead of struggling with various key combinations, consider this.</p><pre><code><code>SUITS = "\N{BLACK CLUB SUIT}\N{WHITE DIAMOND SUIT}\N{WHITE HEART SUIT}\N{BLACK SPADE SUIT}"</code></code></pre><p>This string has the four Unicode characters we want. It can help to use <code>SUITS</code> instead of <code>"&#9827;&#9826;&#9825;&#9824;" </code>in the examples.</p><p>Save the file with a handy name like <code>cards.py</code>. We can then run this script to see the code in action. A lot of IDE&#8217;s have a run button to run the file we&#8217;re working on. Others of us will have a window open to edit the text file and a terminal window open to run the file.</p><p>This little <code>cards.py</code> script doesn&#8217;t do much. But, it helps confirm we&#8217;re on course with the class definition.</p><p>Well.</p><p><em>Adjacent</em> to the right course.</p><p>The <code>points</code> attribute initialization is less than desirable. That <code>rank if rank &lt; 11 else 10 </code>computation seems out of place. It should be a feature of the <code>Card</code> class definition. It doesn&#8217;t seem like it should be a feature of the function that makes a deck of cards.</p><p><strong>Strategy 4. Review and rework the experiment.</strong></p><p>There&#8217;s a set of OO design principles, with the initials <strong>SOLID</strong>. This pushes at the <strong>I</strong> &#8212; Interface Segregation Principle &#8212; a bit. The requirement to compute the points outside the card feels like it imposes too many details on a collaborating class. There are some ways to make it simpler.</p><h3><strong>Refactor a Computation</strong></h3><p>It&#8217;s a bit more polite for <code>Card</code> objects to derive the number of points from the rank. This frees the <code>Deck</code> object from having to know too much about how <code>Card</code> objects are built.</p><p>We&#8217;ll need to insert a method to compute a derived attribute value.</p><p>There are a bunch of ways to do this.</p><ul><li><p>Deep into the bay of &#8220;Not Obvious&#8221;, there&#8217;s a special method called <code>__post_init__()</code> which can be used to compute derived values. This also requires marking the <code>points</code> attribute as a field that is not initialized.</p></li><li><p>Out in plain sight is a method with a name like <code>points()</code> that does the computation. This introduces a slightly different syntax for points. For rank and suit, we use the attribute name: <code>card.rank</code> or <code>card.suit</code>. But for a method, we have to call it like a functionL <code>card.points()</code>. It&#8217;s only a pair of ()&#8217;s. But. It&#8217;s also a shallow spot in the water we have to navigate around.</p></li><li><p>Which leads us to a property. This is a method without the extra syntax.</p></li></ul><p>This seems helpful.</p><pre><code><code>from dataclasses import dataclass

@dataclass
class Card:
    rank: int
    suit: str

    @property
    def points(self) -&gt; int:
         return 10 if self.rank &gt;= 11 else self.rank</code></code></pre><p>Since this is about getting started, we&#8217;re going to avoid too much discussion of how a property works. The <code>self</code> parameter is the reference to the object; allowing the method access to all of the attributes and methods defined by class. When we write <code>starter_card.points</code> in our code, this treated as if we&#8217;d written <code>starter_card.points()</code>, which is essentially <code>Card.points(starter_card)</code>. The argument value for <code>self</code> is the object.</p><p>For simple attributes, we use <code>name: type</code>. For methods we use <code>def name(self, parameters -&gt; type:&#8230;</code>. There&#8217;s more, but that goes beyond getting started.</p><p>Now, we can have this.</p><pre><code><code>def make_cards():
     for rank in range(1, 14):
         for suit in "&#9827;&#9826;&#9825;&#9824;":
             c = Card(rank, suit)
             print(c, c.points)  </code></code></pre><p>That seems much nicer. We can run it and see that we have built objects and displayed their state.</p><p>This little demo function reflects the heart of object-oriented programming. We defined a class of objects. We wrote collaborating code to build instances of that class of objects and interrogate the internal state of the objects.</p><p>The value of points is &#8220;encapsulated&#8221; by the class definition.</p><p>Let&#8217;s extend this a little. We&#8217;re back to strategies 3 and 4 &#8212; experiment and rework.</p><h3><strong>The Deck of Cards</strong></h3><p>The deck of cards undergoes a number of transformations. It&#8217;s shuffled. Cards are dealt into hands. For Cribbage, two hands of six cards must be extracted from the deck, leaving 40 behind. After creating the crib, a &#8220;starter card&#8221; is flipped.</p><p>The <code>Deck</code> is a container of 52 <code>Card</code> objects. We decided to use <code>list[Card]</code> as the type hint, suggesting a stateful list of <code>Card</code> objects would be a good implementation.</p><p>We can leverage Python&#8217;s list comprehension to create a list of <code>Card</code> objects to initialize a <code>Deck</code>.</p><pre><code><code>the_deck = Deck(
    [Card(rank, suit)
        for rank in range(1, 14)
        for suit in "&#9827;&#9826;&#9825;&#9824;"
    ]
)</code></code></pre><p>This builds a list, <code>[Card(rank, suit) for rank in range(1, 14) for suit in "&#9827;&#65038;&#9826;&#9825;&#9824;&#65038;"]</code>; then it initializes a <code>Deck</code> object with the list.</p><p>This seems like it doesn&#8217;t really belong outside the definition of the <code>Deck</code> class. Also. The cards need to be shuffled. We want a better initialization for a <code>Deck</code> object. While the <code>@dataclass</code> decorator creates an initialization method, in this case, we don&#8217;t actually want it.</p><h3><strong>Our Own Initialization</strong></h3><p>Writing our own initialization method means defining a special method named __init__(). This doesn&#8217;t return a value; it sets the attribute values.</p><pre><code><code>from dataclasses import dataclass
import random

@dataclass
class Deck:
    cards: list[Card]

    def __init__(self) -&gt; None:
        self.cards = [
            Card(rank, suit)
            for rank in range(1, 14)
            for suit in "&#9827;&#9826;&#9825;&#9824;"
        ]
        random.shuffle(self.cards)</code></code></pre><p>This seems to cover some of the use cases for a <code>Deck</code> object.</p><p>We can see it in action like this:</p><pre><code><code>def demo_1():
    deck = Deck()
    hand_1 = deck.cards[:6]
    hand_2 = deck.cards[6:12]
    starter = deck.cards[12]

    print("dealer", hand_1)
    print("pone", hand_2)
    print("starter", starter)</code></code></pre><p>This creates two 6-card hands and a starter card. (Yes, the two players are &#8220;dealer&#8221; and &#8220;pone&#8221;; the opponent is called &#8220;pone&#8221; in Cribbage.)</p><h3><strong>Mid-Point Review</strong></h3><p>Where did we start? Where are we now? Where are we headed?</p><p>From a blank screen, we followed a couple of strategies to gather our thoughts in the form of data structures, written out as Python data classes. We added attributes, and experimented. The experiments suggested some methods that would simplify the interface to each class.</p><p>We&#8217;ve got working object-oriented programming without very much struggle. The use of a dataclass means that a number of methods were created, and those methods made experimentation simpler.</p><p>Let&#8217;s look at some more methods that make this a little nicer to work with.</p><h3><strong>Cards are Ugly</strong></h3><p>The a subtle problem with the way cards are displayed. Here&#8217;s the last line of output from the <code>demo_1()</code> function. (Your output may not match; the shuffling is randomized.)</p><pre><code><code>starter Card(rank=11, suit='&#9827;')</code></code></pre><p>This isn&#8217;t generally what we expect. The method created for us by the <code>@dataclass</code> decorator is useful in many ways, but not ideal for what we&#8217;re doing.</p><p>A Python object has two ways to make a string value for display:</p><ul><li><p>The <code>__str__()</code> method, which makes a &#8220;pretty&#8221; string.</p></li><li><p>The <code>__repr__()</code> method, which makes a string that &#8212; frequently &#8212; is an expression that will recreate the object. The string <code>Card(rank=1, suit='&#9827;')</code> will create a new <code>Card</code> instance.</p></li></ul><p>It&#8217;s not clear which method is used when we display an object at the REPL prompt. We can figure out what&#8217;s going on with two built-in functions, <code>str()</code> and <code>repr()</code>. The <code>str()</code> function appeals to an object&#8217;s <code>__str__()</code> method to create a string.</p><p>We&#8217;ll switch to the interactive Python just to show another exploration technique.</p><pre><code><code>&gt;&gt;&gt; from cards import *
&gt;&gt;&gt; deck = Deck()
&gt;&gt;&gt; str(deck.cards[0])
"Card(rank=3, suit='&#9826;')"
&gt;&gt;&gt; repr(deck.cards[1])
"Card(rank=6, suit='&#9824;')"</code></code></pre><p>The internal <code>__str__()</code> and <code>__repr__()</code> methods are both doing the same thing. We need to override the <code>__str__()</code> method to produce &#8220;pretty&#8221; output.</p><p>Here&#8217;s one way to do the string conversion:</p><pre><code><code>def __str__(self) -&gt; str:
    named = {1: "A", 11: "J", 12: "Q", 13: "K"}
    rank = named.get(self.rank, str(self.rank))
    return f"{rank:&gt;2s}{self.suit}"</code></code></pre><p>The <code>named</code> dictionary maps ranks to name strings for those few cards with names. The second argument value to <code>named.get()</code> is an object to use if there&#8217;s no matching key in the dictionary. The output is a formatted string with rank and suit.</p><p>After this change, we&#8217;ll see this when we fire up interactive Python.</p><pre><code><code>&gt;&gt;&gt; str(deck.cards[0])
' 3&#9826;'</code></code></pre><p>This is much nicer. It&#8217;s not the whole story, though.</p><p>Sadly, the way lists are printed doesn&#8217;t really do what we want. Take a look at this:</p><pre><code><code>&gt;&gt;&gt; hand_1 = deck.cards[:6] 
&gt;&gt;&gt; print(hand_1) 
[Card_P(rank=3, suit='&#9826;'), Card_P(rank=6, suit='&#9824;'), Card_P(rank=7, suit='&#9826;'), Card_P(rank=1, suit='&#9824;'), Card_P(rank=6, suit='&#9826;'), Card_P(rank=10, suit='&#9825;')]  </code></code></pre><p>This is still kind of awful.</p><p>What can we do?</p><p>We have two choices:</p><ul><li><p>Write some function to use an expression like <code>', '.join(str(c) for c in hand_1)</code> to use the str() function when displaying a hand of cards.</p></li><li><p>Pause a moment. (No. I don&#8217;t mean put up with ugly output.)</p></li></ul><p>Let&#8217;s take a step back from this problem. What are we trying to do?</p><h3><strong>Finding New Classes</strong></h3><p>We need to format the hand of cards. When we started we used <code>dealer_hand = deck.cards[:6]</code> to put six cards into the dealer&#8217;s hand.</p><p>What&#8217;s the type hint for <code>dealer_hand</code>? It&#8217;s <code>list[Card]</code>.</p><p>This is Python&#8217;s strength as well as a source of potential weakness: we have so many built-in types, we often forget what they are.</p><p>A <code>Hand</code> &#8212; like a <code>Deck</code> &#8212; is more than a list of cards.</p><p>We&#8217;ve uncovered a new class. We need to refactor our code to replace the built-in class with a new class.</p><p><strong>Strategy 5. Refactor classes to narrow responsibilities.</strong></p><p>In this case, the built-in <code>list[Card]</code> isn&#8217;t right, and requires the collaborator to do the formatting. It seems better to define a <code>Hand</code> class that does this in a single, tidy package.</p><p>Something like this:</p><pre><code><code>@dataclass
class Hand:
    cards: list[Card]
    
    def __str__(self) -&gt; str:
        return ', '.join(str(c) for c in self.cards)</code></code></pre><p>The class only has one special method.</p><p>Interestingly, it looks somewhat like the <code>Deck</code> class. Sometimes, this is a significant overlap, and perhaps the two classes really do share some common code. In this case, it looks more like a coincidence. There are profound differences:</p><ul><li><p>The <code>Deck</code> class builds its own set of 52 cards and shuffles them.</p></li><li><p>The <code>Hand</code> class is given 6 cards, and will donate 2 cards to the crib. There&#8217;s no compelling reason to shuffle a <code>Hand</code> of cards. Indeed, because of the way Cribbage is played, there will be numerous analyses and decisions around these cards.</p></li></ul><h3><strong>What&#8217;s Next?</strong></h3><p>We started with a blank screen, filled in some dataclasses, and ran some experiments.</p><p>We would up with a tidy little object model that seems to capture some important parts of the problem domain. We could depict it like this, using a tool like PlantUML.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!srMA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!srMA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 424w, https://substackcdn.com/image/fetch/$s_!srMA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 848w, https://substackcdn.com/image/fetch/$s_!srMA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 1272w, https://substackcdn.com/image/fetch/$s_!srMA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!srMA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic" width="285" height="234" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:234,&quot;width&quot;:285,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8040,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://stevenlott.substack.com/i/169317147?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!srMA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 424w, https://substackcdn.com/image/fetch/$s_!srMA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 848w, https://substackcdn.com/image/fetch/$s_!srMA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 1272w, https://substackcdn.com/image/fetch/$s_!srMA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb22fb07d-5704-4132-9791-20051ee3dab9_285x234.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This kind of UML diagram can help someone visualize the relationships among the classes. There isn&#8217;t a great way to depict properties in UML, so we included a stereotype, <code>&#171;property&#187;</code>, which might be helpful.</p><p>One potential next step is going to be discarding two cards from a <code>Hand</code> to make the dealer&#8217;s crib. This is a common step after the deal and before play and scoring.</p><p>Early on, we described the problem without any useful details. &#8220;&#8230;trying to settle an argument over best ways to play Cribbage.&#8221; What argument? Before doing any more programming, we need to spend a moment detailing the argument. What&#8217;s the state of play? What are the alternative strategies? What information do we need to understand the outcomes?</p><p>It&#8217;s time to go back to the triple-quoted docstring at the time of the <code>cards.py</code> module and jot down some more ideas. Sometimes it helps to try and organize the ideas.</p><p>This part of the work is not object-oriented programming. These are brain calories being burned to understand the problem domain. Understanding these details &#8212; and then figuring a way to model them in software &#8212; is one of the most difficult aspects of programming. Creating an object-oriented implementation with dataclasses isn&#8217;t quite as difficult as understanding the problem and providing information a person can use to maximize their cribbage scores.</p><div><hr></div><p><strong>In Part 2 we will cover:</strong></p><ul><li><p><strong>Immutability choices:</strong> <code>@dataclass(frozen=True)</code> vs <code>NamedTuple</code>&#8212;what changes and why it matters.</p></li><li><p><strong>Hashability in practice:</strong> why sets and dict keys force the issue; reading <em>&#8220;TypeError: unhashable type&#8221;</em> as a design hint.</p></li><li><p><code>TypedDict</code><strong> for schema-first code:</strong> when a heterogenous record beats a class.</p></li><li><p><strong>Python&#8217;s duck typing, properly used:</strong> Protocols to describe behavior, not lineage.</p></li><li><p><strong>Dependency injection without frameworks:</strong> class attributes + Protocols to remove hard-wired types (our <code>Deck</code> &#8594; <code>Card</code> refactor).</p></li><li><p><strong>Tooling that pays off:</strong> mypy/pyright to lock interfaces without slowing you down.</p></li></ul><p>We&#8217;ll finish by generalising <code>Hand</code> and <code>Deck</code>, then return to the Cribbage simulation with a design that&#8217;s easier to extend and test.</p><div><hr></div><p><strong>About the Author:</strong></p><p><strong>Steven Lott</strong> has been programming since computers were large, expensive, and rare. Working for decades in high tech has given him exposure to a lot of ideas and techniques, some bad, but most are helpful to others. Since the 1990s, Steven has been engaged with Python, crafting an array of indispensable tools and applications. His profound expertise has led him to contribute significantly to Packt Publishing, penning notable titles like <em><strong><a href="https://www.packtpub.com/en-us/product/mastering-object-oriented-python-9781789531404">Mastering Object-Oriented Python</a> </strong></em>(Packt, 2019), <em><strong><a href="https://www.packtpub.com/en-us/product/modern-python-cookbook-9781835466384">Modern Python Cookbook</a></strong></em> (Packt, 2024), and <em><strong><a href="https://www.packtpub.com/en-us/product/python-real-world-projects-9781803246765">Python Real-World Projects</a> </strong></em>(Packt, 2023). A self-proclaimed technomad, Steven's unconventional lifestyle sees him residing on a boat, often anchored along the vibrant east coast of the US. He tries to live by the words &#8220;Don&#8217;t come home until you have a story.&#8221;</p><div><hr></div><p>Lott is currently working on the <strong>5th Ed.</strong> of <em><strong>Python Object-Oriented Programming</strong> </em>due later this year. If you liked what you read in this article, and would like to read more while you wait for the new edition, you can check out <strong><a href="https://www.packtpub.com/en-us/product/python-object-oriented-programming-9781801075237">Python Object-Oriented Programming, 4th Ed. (Packt, 2021)</a>.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://www.packtpub.com/en-us/product/python-object-oriented-programming-9781801075237" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S24_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 424w, https://substackcdn.com/image/fetch/$s_!S24_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 848w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1272w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000" width="340" height="419.75308641975306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0059565-ab59-4421-90dd-de189e63939f_810x1000&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1000,&quot;width&quot;:810,&quot;resizeWidth&quot;:340,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Object-Oriented Programming&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://www.packtpub.com/en-us/product/python-object-oriented-programming-9781801075237&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Object-Oriented Programming" title="Python Object-Oriented Programming" srcset="https://substackcdn.com/image/fetch/$s_!S24_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 424w, https://substackcdn.com/image/fetch/$s_!S24_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 848w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1272w, https://substackcdn.com/image/fetch/$s_!S24_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0059565-ab59-4421-90dd-de189e63939f_810x1000 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here is what some readers have said:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B_ZW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 424w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 848w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1272w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png" width="1078" height="432" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:432,&quot;width&quot;:1078,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95317,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B_ZW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 424w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 848w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1272w, https://substackcdn.com/image/fetch/$s_!B_ZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0076979-6368-4943-8ac8-1595f1bdb737_1078x432.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QhcA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QhcA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 424w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 848w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1272w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png" width="1097" height="495" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:495,&quot;width&quot;:1097,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:120161,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QhcA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 424w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 848w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1272w, https://substackcdn.com/image/fetch/$s_!QhcA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6d263b1-d57e-44df-bbb1-6d36e59a7807_1097x495.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bMcY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bMcY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 424w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 848w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1272w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png" width="1070" height="365" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:365,&quot;width&quot;:1070,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:81939,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://deepengineering.substack.com/i/169634247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bMcY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 424w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 848w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1272w, https://substackcdn.com/image/fetch/$s_!bMcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f8102a-90fa-4688-b30c-92128ca51d95_1070x365.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p>]]></content:encoded></item><item><title><![CDATA[Implementing a Diff Algorithm in MoonBit]]></title><description><![CDATA[A hands-on introduction to MoonBit through the implementation of a version-control-grade diff tool.]]></description><link>https://deepengineering.net/p/implementing-a-diff-algorithm-in</link><guid isPermaLink="false">https://deepengineering.net/p/implementing-a-diff-algorithm-in</guid><dc:creator><![CDATA[Zihang]]></dc:creator><pubDate>Wed, 04 Jun 2025 06:03:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3f4eb0b8-c85e-4c10-a6e0-562a81671e97_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>MoonBit is an emerging programming language that has a robust toolchain and relatively low learning curve. As a modern language, MoonBit includes a formatter, a plugin supporting VSCode, an LSP server, a central package registry, and more. It offers the friendly features of functional programming languages with manageable complexity.</p><p>To demonstrate MoonBit&#8217;s capabilities, we&#8217;ll implement a core software development tool&#8212;a <em>diff algorithm</em>. Diff algorithms are essential in software development, helping identify changes between different versions of text or code. They power critical tools in version control systems, collaborative editing platforms, and code review workflows, allowing developers to track modifications efficiently. If you have ever used git diff then you are already familiar with such algorithms.</p><p>The most widely used approach is Eugene W. Myers Diff algorithm, proposed in the paper <a href="https://link.springer.com/article/10.1007/BF01840446">&#8220;An O(ND) Difference Algorithm and Its Variations&#8221;</a>. This algorithm is widely used for its optimal time complexity. Its space-efficient implementation and ability to find the shortest edit script make it superior to alternatives like patience diff or histogram diff and make it the standard in version control systems like Git and many text comparison tools such as Meld.</p><p>In this tutorial, we&#8217;ll implement a version of the Myers Diff algorithm in MoonBit. This hands-on project is ideal for beginners exploring MoonBit, offering insight into version control fundamentals while building a tool usable by both humans and AI through a standard API.</p><p>We will start by developing the algorithm itself, then build a command line application that integrates the <a href="https://component-model.bytecodealliance.org/">Component Model</a> and the <a href="https://modelcontextprotocol.io/introduction">Model Context Protocol</a> , leveraging MoonBit&#8217;s WebAssembly (Wasm) backend. Wasm is a blooming technology that provides privacy, portability, and near-native performance by running assembly-like code in virtual machines across platforms &#8212;qualities that MoonBit supports natively, making the language well-suited for building efficient cross-platform tools.</p><p>By the end of this tutorial, you&#8217;ll have a functional diff tool that demonstrates these capabilities in action.</p><h1>Project Setup</h1><p>Let&#8217;s first create a new moonbit project by running:</p><pre><code>moon new --lib diff </code></pre><p>The following will be the project structure of the code. The <code>moon.mod.json</code> contains the configuration for the project, while the <code>moon.pkg.json</code> contains the configuration for each package. <code>top.mbt</code> is the file we'll be editing throughout this post.</p><pre><code>&#9500;&#9472;&#9472; LICENSE
&#9500;&#9472;&#9472; moon.mod.json
&#9500;&#9472;&#9472; README.md
&#9492;&#9472;&#9472; src
    &#9500;&#9472;&#9472; lib
    &#9474;   &#9500;&#9472;&#9472; hello.mbt
    &#9474;   &#9500;&#9472;&#9472; hello_test.mbt
    &#9474;   &#9492;&#9472;&#9472; moon.pkg.json
    &#9500;&#9472;&#9472; moon.pkg.json
    &#9492;&#9472;&#9472; top.mbt</code></pre><p>We will be comparing two pieces of text, divided each into lines. Each line will include its content and a line number. The line number helps track the exact position of changes, providing important context about the location of changes when displaying the differences between the original and modified files.</p><pre><code>pub(all) struct Line {
  number : Int
  content: @string.View
} derive(Show)</code></pre><p>The diff algorithm works by generating an edit script that transforms sequence A (the original version) into a sequence B (the new version). The edit script includes three operations: insertion, deletion or retention.</p><pre><code>pub(all) enum Edit {
  Insert(Line)
  Keep(Line, Line)
  Delete(Line)
} derive(Show)</code></pre><p>Thus, here&#8217;s the <strong>diff</strong> function signature:</p><pre><code>pub fn diff(origin: Array[Line], new: Array[Line]) -&gt; Array[Edit] {
  ...
}</code></pre><p>The goal is to minimize insertions and deletions to produce a meaningful diff. A meaningful diff prioritizes human readability by identifying the set of changes needed to transform one text into another &#8212;preserving unchanged sections and highlighting only the actual modifications. This makes it much easier for developers to quickly understand what changed between versions.</p><h1>Applying the Diff Algorithm</h1><p>Before diving into the implementation, let&#8217;s walk through a simple example to illustrate how the diff algorithm detects and represents changes between two versions of text.</p><p>Let's first define a test case that will guide our implementation:</p><pre><code>test {
  let origin =
    #|a
    #|c
  let new =
    #|a
    #|b
    #|c
  inspect!(
    EditScript(
      diff(
        origin
        .split("\n")
        .collect()
        .mapi(fn(index, content) { { number: index, content } }),
        new
        .split("\n")
        .collect()
        .mapi(fn(index, content) { { number: index, content } }),
      ),
    ),
    content=
      #| a
      #|+b
      #| c
      #|
    ,
  )
}</code></pre><p>We certainly don't want to see the following result as it is harder to read; it doesn't highlight the critical change clearly.</p><pre><code>-a
-c
+a
+b
+c</code></pre><p>Before implementing an optimized approach, let us first explore a simple but inefficient depth-first search (DFS) method to understand the core problem of computing diffs.</p><h2>Naive DFS Implementation</h2><p>A straightforward approach tries all possible transformations and selects the shortest path:</p><pre><code>pub fn diff(origin : Array[Line], new : Array[Line]) -&gt; Array[Edit] {
  fn dfs(
    origin : ArrayView[Line],
    new : ArrayView[Line],
    ops : @immut/array.T[Edit]
  ) -&gt; @immut/array.T[Edit] {
    match (origin, new) {
      ([], []) =&gt; return ops
      ([], _) =&gt;
        return ops.concat(@immut/array.from_array(new.map(Edit::Insert)))
      (_, []) =&gt;
        return ops.concat(@immut/array.from_array(origin.map(Edit::Delete)))
      ([origin_head, .. origin_tail], [new_head, .. new_tail]) =&gt; {
        if origin_head.content == new_head.content {
          return dfs(
            origin_tail,
            new_tail,
            ops.push(Edit::Keep(origin_head, new_head)),
          )
        }
        let insert = dfs(origin, new_tail, ops.push(Edit::Insert(new_head)))
        let delete = dfs(origin_tail, new, ops.push(Edit::Delete(origin_head)))
        if insert.length() &lt; delete.length() {
          return insert
        } else {
          return delete
        }
      }
    }
  }

  dfs(origin[:], new[:], @immut/array.new()).to_array()
}</code></pre><p>This method is inefficient for large inputs due to exponential time complexity. For each position where characters don't match, the algorithm explores two possible paths (insert or delete), resulting in O(2^(n+m)) worst-case complexity. Without memoization, it redundantly computes the same subproblems. For real-world scenarios with substantial text differences, this approach quickly becomes impractical, consuming excessive memory and processing time.</p><p>So now, it's time to explore the Myers Diff algorithm which improves efficiency by greedily searching for the shortest path. The following implementation illustrates the core concept of the algorithm rather than providing a direct 1:1 copy from the original paper.</p><h2>Myers Diff</h2><p>Myers algorithm models the problem as an editing graph, where each state <code>(x, y)</code> represents x deletions and y insertions. The shortest path from <code>(0, 0)</code> to <code>(n, m)</code> minimizes operations.</p><p>The graph below visualizes the editing process between two sequences:</p><pre><code>  0   1   2   3   4   5   6   7
0 o---o---o---o---o---o---o---o
  |   |   | \ |   |   |   |   | -C
1 o---o---o---o---o---o---o---o
  |   | \ |   |   | \ | \ |   | -B
2 x---o---o---o---o---o---o---o
  | \ |   |   | \ |   |   | \ | -A
3 o---x---o---o---o---o---o---o
  |   | \ |   |   | \ | \ |   | -B
4 o---o---x---o---o---o---o---o
  | \ |   |   | \ |   |   | \ | -A
5 o---o---o---x---o---o---o---o
  |   |   | \ |   |   |   |   | -C
6 o---o---o---o---x---o---o---o
   +A  +B  +C  +A  +B  +B  +A</code></pre><p>Myers' algorithm follows a few key principles:</p><ul><li><p>It greedily retains common elements, because Keep operations have no cost.</p></li><li><p>It prioritizes paths with fewer insertions and deletions to minimize the number of operations.</p></li><li><p>It tracks the optimal paths along diagonals, ensuring efficient computation of the shortest transformation sequence.</p></li></ul><p>So, here&#8217;s the naive implementation based on these principles:</p><pre><code>pub fn diff(origin: Array[Line], new: Array[Line]) -&gt; Array[Edit] {
  struct Vertex {
    x: Int
    y: Int
  } derive(Eq, Hash, Show)

  let n = origin.length()
  let m = new.length()

  // A hashmap to keep track of the best path reached on each diagonal
  let mut map = { 0: ({ x: 0, y: 0 }, @immut/array.new()) }

  for d in 0..&lt;(n + m) {
    let next_map: Map[Int, (Vertex, @immut/array.T[Edit])] = {}

    for _, edits in map {
      // Greedily try to keep the longest common subsequence
      let mut x = edits.0.x
      let mut y = edits.0.y
      let mut edits = edits.1

      while x &lt; n &amp;&amp; y &lt; m &amp;&amp; origin[x].content == new[y].content {
        edits = edits.push(Keep(origin[x], new[y]))
        x += 1
        y += 1
      }

      // Check if we've reached the goal
      if x == n &amp;&amp; y == m {
        return edits.to_array()
      }

      // Try to delete the original line
      if x &lt; n {
        let next_vertex = { x: x + 1, y }
        let next_edits = edits.push(Delete(origin[x]))

        if next_map[x + 1 - y] is None ||
           (next_map[x + 1 - y] is Some(({ x, y: _ }, _)) &amp;&amp; next_vertex.x &gt; x) {
          next_map[x + 1 - y] = (next_vertex, next_edits)
        }
      }

      // Try to insert the new line
      if y &lt; m {
        let next_vertex = { x, y: y + 1 }
        let next_edits = edits.push(Insert(new[y]))

        if next_map[x - y - 1] is None ||
           (next_map[x - y - 1] is Some(({ x: _, y }, _)) &amp;&amp; next_vertex.y &gt; y) {
          next_map[x - y - 1] = (next_vertex, next_edits)
        }
      }
    }

    map = next_map
  }

  // Unreachable
  panic()
}</code></pre><p>Notice that this implementation is not the most efficient, as it explores each previous case twice. In practice, the algorithm can be optimized to process once per diagonal.</p><p>There&#8217;s also a variant of Myers&#8217; algorithm that uses linear memory. For a detailed explanation, refer to the MoonBit documentation on <a href="https://docs.moonbitlang.com/en/latest/example/myers-diff/myers-diff3.html">Myers diff part 3</a>.</p><h1>Building a CLI application</h1><p>Having a library is nice, it&#8217;s time to put it into action, by creating an application.</p><p>Let&#8217;s create a new MoonBit project by cloning the CLI template repository:</p><pre><code>git clone https://github.com/peter-jerry-ye/cli-template.git</code></pre><p>Alternatively, you can also create a repository from the template directly on GitHub and then clone your own version.</p><p>Notice that even though we are creating an &#8220;executable&#8221;, there is no main function. Instead, we are developing a &#8220;library&#8221; that imports and exports specific functions based on the <code>wasi-cli</code>, the standard API set for command-line interfaces in Wasm applications. This setup imports functions such as <code>read</code> and <code>write</code>, while exporting an entry function that serves as the program&#8217;s starting point.</p><p>Next, let us add a local dependency pointing to the library that we&#8217;ve just built by editing the <code>moon.mod.json file</code>:</p><pre><code>{
  "name": "moonbit-community/cli-template",
  "deps": {
    "peter-jerry-ye/wasi-imports": "0.1.4",
    "peter-jerry-ye/io": "0.3.2",
    "peter-jerry-ye/async": "0.1.6",
    // check the `moon.mod.json` of the previous project
    "username/hello": {
      // path to the other project root
      "path": "../diff"
    }    
  },
  "source": "src"
}</code></pre><p>With the CLI project set up and the diff library linked as a local dependency, we&#8217;re ready to build a functional command-line interface. The next step is to handle user input, read files, and display the diff results&#8212;making the tool usable for humans.</p><h2>Building a CLI for Human Interaction</h2><p>This CLI application will:</p><ol><li><p>Retrieve command-line arguments</p></li><li><p>Read input files</p></li><li><p>Compute the Diff</p></li><li><p>Display the result</p></li></ol><h3>Getting arguments</h3><p>First, we will retrieve the command line arguments using the <code>get_arguments</code> function from the <code>environment</code> package.</p><p>The <code>top</code> function in <code>cli/src/stub.mbt</code> retrieves the arguments and validates them:</p><pre><code>async fn top() -&gt; Unit! {
  let args = @environment.get_arguments()
  guard args is [_arg, origin, current] else {
    @io.println!("Usage: diff &lt;origin&gt; &lt;current&gt;")
  }
  @io.println!("Diffing \{origin} and \{current}")
}</code></pre><p>The<em> </em><code>cli/src/moon.pkg.json</code><em> </em>file should include the following import for the environment interface of <code>wasi-cli</code>:</p><pre><code>{
  // "link": { ... },
  "import": [
    // ...
    // add import environment interface of wasi-cli
    "peter-jerry-ye/wasi-imports/interface/wasi/cli/environment"
  ]
}</code></pre><p>Although asynchronous I/O is not strictly necessary here, the current <strong>io</strong> package provides only an async API, so we use it for compatibility.</p><p>Next, let us try to execute our program. As we are building a Wasm module that uses the <strong>WASI-CLI</strong> (a standard API for command line interfaces), we need <strong><a href="https://github.com/bytecodealliance/wasm-tools">wasm-tools</a></strong> to convert our Wasm to embed the necessary interfaces and <a href="https://wasmtime.dev/">wasmtime</a> to run the program. The following commands build and execute the Wasm component:</p><pre><code># Build Wasm
moon build --target wasm --debug
wasm-tools component embed --encoding utf16 wit target/wasm/debug/build/cli-template.wasm -o target/cli.core.wasm
wasm-tools component new target/cli.core.wasm -o target/cli.wasm
# Run the program
wasmtime target/cli.wasm a.mbt b.mbt</code></pre><p>This should produce the following output:</p><pre><code>Diffing a.mbt and b.mbt</code></pre><h3>Reading files</h3><p>Next, we&#8217;ll read the input files specified as command-line arguments.</p><p>For this step, we need additional imports to enable filesystem access and handle file content encoding:</p><pre><code>{
  // ...
  "import": [
    // ...
    "peter-jerry-ye/wasi-imports/interface/wasi/filesystem/preopens",
    "peter-jerry-ye/wasi-imports/interface/wasi/filesystem/types",
    "peter-jerry-ye/wasi-imports/interface/wasi/io/streams",
    "tonyfettes/encoding"
  ]
}</code></pre><p>Next, we define a function that will read a file from a provided path. The function retrieves the preopened directory (passed to the runtime), opens the file, reads its contents via an input stream, and converts the content from UTF-8 to UTF-16 encoding (the internal encoding for MoonBit Strings):</p><pre><code>async fn read(path : String) -&gt; String! {
  let decoder = @encoding.decoder(UTF8)
  // Get the directory
  let directories = @preopens.get_directories()
  guard directories is [(root, _), ..] else {
    fail!("No preopen directories found.")
  }
  // Get the file within the directory
  let file = root
    .open_at(
      @types.PathFlags::default(),
      path,
      @types.OpenFlags::default(),
      @types.DescriptorFlags::default().set(@types.DescriptorFlagsFlag::READ),
    )
    .unwrap_or_error!()
  // Get the input stream
  let input_stream = file.read_via_stream(0).unwrap_or_error!()
  // Read the full content and decode UTF8 to UTF16 String
  let content = decoder.decode!( @io.read_all!(stream=input_stream).contents())
  input_stream.drop()
  file.drop()
  content
}</code></pre><p><em><strong>Note: </strong></em>For simplicity, this function does not include robust error handling &#8212;the program will exit and print an error message if something goes wrong. Additionally, it assumes that the files to be diffed reside within the same directory, which must be accessible to the Wasm runtime due to the sandboxed environment and &#8220;deny by default&#8221; security principle.</p><p>Next, let us update the <strong>top</strong> function to read the file contents:</p><pre><code>async fn top() -&gt; Unit! {
  let args = @environment.get_arguments()
  guard args is [_arg, origin, current] else {
    @io.println!("Usage: diff &lt;origin&gt; &lt;current&gt;")
  }
  // @io.println!("Diffing \{origin} and \{current}")
  let origin_content = read!(origin)
  let current_content = read!(current)
}</code></pre><p>Now, we can rerun the previous build commands and execute the program as before. This time we must provide a directory path (assuming `A.txt` and `B.txt` exist in that directory) because Wasm modules execute within sandboxed environments that follow a "deny by default" security model. Without explicitly granting access via runtime flags, the program can literally access nothing:</p><pre><code># Rerun the previous build commands
moon build --target wasm --debug
wasm-tools component embed --encoding utf16 wit target/wasm/debug/build/cli-template.wasm -o target/cli.core.wasm
wasm-tools component new target/cli.core.wasm -o target/cli.wasm
# Execute the Wasm
wasmtime run --dir . target/cli.wasm A.txt B.txt</code></pre><h3>Displaying the Diff results</h3><p>The final step is to integrate the diff algorithm and display the results. First, let us import the diff package into your project:</p><pre><code>{
  // ...
  "import": [
    // ...
    {
      "path": "username/hello",
      "alias": "diff"
    }
  ]
}</code></pre><p>Next, let us use the diff package to compare the files and print the result. ANSI escape codes are used to add color, making insertions and deletions easier to distinguish:</p><pre><code>async fn top() -&gt; Unit! {
  ...
  let origin_content = read!(origin)
  let current_content = read!(current)
  let origin_lines : Array[@diff.Line] = origin_content
    .split("\n")
    .collect()
    .mapi(fn(i, content) { { number: i, content } })
  let current_lines : Array[@diff.Line] = current_content
    .split("\n")
    .collect()
    .mapi(fn(i, content) { { number: i, content } })
  let script = @diff.diff(origin_lines, current_lines)
  for edit in script {
    match edit {
      Insert(value)  =&gt; @io.println!("\u001B[32m+\{value.content}")
      Keep(value, _) =&gt; @io.println!("\u001B[37m \{value.content}")
      Delete(value)  =&gt; @io.println!("\u001B[31m-\{value.content}")
    }
  }
}</code></pre><p>With this, the CLI tool is complete.</p><h2>Building the CLI for AI integration</h2><p>While the command-line tool we have created works well for human users &#8212;and for AIs that can call the CLI tools&#8212;supporting the Model Context Protocol (MCP) makes it accessible to a broader range of AI systems. MCP defines how tools integrate with AI clients, enabling features like prompt generation and autocompletions. With MCP, our diff tool can be used seamlessly by any client that supports the protocol, such as <strong>VSCode</strong>, <strong>Cherry Studio</strong>, and more.</p><p>In this section, we&#8217;ll use the <strong>MCP server SDK</strong> for MoonBit to register our tool and launch the server, allowing AI clients to integrate and use the diff functionality.</p><p>Let us add the <code>mcp-server</code> sdk by running:</p><pre><code>moon add peter-jerry-ye/mcp-server</code></pre><p>And add the following import to our <code>moon.pkg.json</code>:</p><pre><code>{
  // ...
  "import": [
    // ...
    "peter-jerry-ye/async/stream",
    {
      "path": "peter-jerry-ye/mcp-server",
      "alias": "mcp"
    },
    "peter-jerry-ye/mcp-server/schema"
  ]
}</code></pre><p>Now, let us remove the <code>top</code> function and setup a <code>run</code> function that defines the tool and launches the server. Each tool requires a name, a description, and an input schema (parameters are passed as JSON). We will also need to define a callback to handle incoming requests.</p><pre><code>///| Run the program.
pub fn run() -&gt; Result[Unit, Unit] {
  // Create a new server
  let server = @mcp.new(Bytes::from_fixedarray(@random.get_random_bytes(32)))
  // Add the tool
  server.add_tool(
    name="diff",
    description="Diff two files under the current repository",
    // Define the input by using json schema
    inputSchema=@schema.object({
      "origin": @schema.string(),
      "current": @schema.string(),
    }),
    // Define the callback for handling the request
    cb=async fn(args, _server) {
      guard args is { "origin": String(origin), "current": String(current), .. } else {
        return {
          isError: true,
          payload: [Text(text="Usage: diff &lt;origin&gt; &lt;current&gt;")],
        }
      }
      // Handle error just in case the files aren't found correctly
      let (origin_content, current_content) = try {
        (read!(origin), read!(current))
      } catch {
        error =&gt;
          return {
            isError: true,
            payload: [Text(text="Failed to read the file: \{error}")],
          }
      }
      let origin_lines : Array[@diff.Line] = origin_content
        .split("\n")
        .collect()
        .mapi(fn(i, content) { { number: i, content } })
      let current_lines : Array[@diff.Line] = current_content
        .split("\n")
        .collect()
        .mapi(fn(i, content) { { number: i, content } })
      let script = @diff.diff(origin_lines, current_lines)
      let builder = StringBuilder::new()
      for edit in script {
        match edit {
          Insert(value) =&gt; builder.write_string("+ \{value.content}\n")
          Keep(value, _) =&gt; builder.write_string("  \{value.content}\n")
          Delete(value) =&gt; builder.write_string("- \{value.content}\n")
        }
      }
      { isError: false, payload: [Text(text=builder.to_string())] }
    },
  )
  server.serve(
    stdin=@stream.input_stream_reader(@io.stdin_stream),
    stdout=@stream.output_stream_writer(@io.stdout_stream),
    stderr=@stream.output_stream_writer(@io.stderr_stream),
  )
  @io.event_loop.run()
  Ok(())
}</code></pre><p>Take VS Code as an example MCP client. Configure MCP servers with the following settings in the file <code>.vscode/mcp.json</code>:</p><pre><code>{
  "servers": {
    "my-diff-server": {
      "type": "stdio",
      "command": "wasmtime",
      "args": [
        "--dir",
        "cli", // path to the workspace where the files to be diffed exists
        "cli/target/cli.wasm" // path to the cli.wasm
      ]
    }
  }
}</code></pre><p>With this setup, AI clients can now invoke the diff tool to compare two files directly.</p><h1>Conclusion</h1><p>In this article, we demonstrated how to implement a diff algorithm in <strong>MoonBit</strong> and transformed it into a fully functional tool&#8212;usable by both humans via the CLI and AI systems via the <strong>MCP</strong>. This project showcased MoonBit&#8217;s capabilities for building efficient, portable applications with WebAssembly.</p><p>Feel free to explore further enhancements, such as optimizing the diff algorithm or extending the tool for new AI clients.</p><p></p>]]></content:encoded></item></channel></rss>