Table of Contents
- Why Configuration Matters (Even More in Production)
-
The Approach: Standing on Python's Shoulders
- Starting Point: Survey the Landscape
- The Template: python-dotenv to Guide Development
- Progressive Implementation: Walking Before Running
- Foundation: Parse KEY=value
- Validation: Python Interop Testing
- Adding Quotes, Comments, and Whitespace Handling
- Advanced Features: Variable Expansion and Multiline Values
- The Tricky Bit: Multiline Quote Detection
- Code Review: Finding the Hidden Bugs
- Outcome: A Production-Ready Library
- Key Lessons for Building Mojo Libraries
- Real-World Impact: Configuration at Scale
- Try It Yourself
- What's Next?
- A Note on AI-Assisted Development
- Closing Thoughts
Lessons from building my first production Mojo package mojo-dotenv: bringing .env to Mojo with near-100% python-dotenv compatibility.
Why Configuration Matters (Even More in Production)
If you've worked with Python applications, you've likely used python-dotenv to manage environment variables. It's the de facto standard for keeping configuration separate from code. But as businesses adopt Mojo for high-performance workloads, this foundational tooling needs to come along for the ride.
Enter my mojo-dotenv package: a modern, production-ready .env parser and loader for Mojo, achieving 98%+ compatibility with python-dotenv whilst leveraging Mojo's performance advantages.
Why not just use python-dotenv via Mojo's Python interop? You certainly could. For a configuration library that runs once at startup, Python interop would be perfectly adequate. Native Mojo implementations make the most compelling case for critical-path operations: numerics, tensor operations, tight loops where every nanosecond counts. But building native Mojo libraries still offers advantages even for "cold-path" code: zero Python runtime dependency, faster startup (no interpreter initialisation), cleaner deployment (pure Mojo binaries), and crucially for me—a deep learning opportunity. Understanding Mojo's string handling, ownership model, and standard library by building something real beats reading docs any day.
The Approach: Standing on Python's Shoulders
Starting Point: Survey the Landscape
Before writing a single line of code, I surveyed the existing Mojo ecosystem. The community had already produced mojoenv by itsdevcoffee, a pioneering effort that brought .env support to early Mojo.
However, mojoenv was built for Mojo in 2023. Running it against a more recent Mojo (version 25.7+) not unsurprisingly revealed various breaking changes. Rather than patching legacy code against a moving target, I decided to build fresh, aim for python-dotenv compatibility.
The Template: python-dotenv to Guide Development
Why reinvent the wheel? python-dotenv has some years of production hardening (> 8.5k GitHub stars), edge case discovery, and community feedback. It's the reference implementation.
My strategy:
- Study
python-dotenv's behaviour — Not just the docs, but actual testing - Implement progressively — Start simple, add complexity incrementally
- Validate continuously — Compare Mojo output against Python output
This meant I could skip the "figure out edge cases" phase. It greatly simplified my job: implement in Mojo, verify compatibility.
Progressive Implementation: Walking Before Running
Foundation: Parse KEY=value
Started with the absolute basics:
# tests/fixtures/basic.env
KEY1=value1
DATABASE_URL=postgresql://localhost/db
PORT=8080
Core functions:
parse_line()— Handle a single linedotenv_values()— Parse entire file to Dictload_dotenv()— Load into environment
Key insight: Mojo's StringSlice vs String distinction required careful handling.
Unlike Python where strings are simply strings, Mojo distinguishes between:
String— An owned string that manages its own memoryStringSlice— A lightweight "view" into string data (zero-copy, no allocation)
Most string operations (.strip(), .split(), subscripting) return StringSlice for efficiency, they reference existing memory rather than copying. But when you need to store the result or pass it to functions expecting String, you must explicitly convert:
var line: String = " KEY=value "
var stripped_slice = line.strip() // Returns StringSlice
var stripped: String = String(stripped_slice) // Explicit conversion
// Or more concisely:
var stripped = String(line.strip()) // StringSlice -> String
This is similar to Rust's &str (view) vs String (owned), or C++'s string_view vs string. The benefit: massive performance gains from avoiding unnecessary copies during parsing. The cost: explicit conversions when ownership matters.
For mojo-dotenv, nearly every line parse required this pattern: strip whitespace, extract key/value, store in Dict. Getting the String/StringSlice conversions right was essential.
Validation: Python Interop Testing
Created test_python_compat.mojo that loads the same .env file in both Mojo and Python, then compares:
from python import Python
fn main() raises:
# Load in Python
var py_dotenv = Python.import_module("dotenv")
var py_result = py_dotenv.dotenv_values("tests/fixtures/basic.env")
# Load in Mojo
var mojo_result = dotenv_values("tests/fixtures/basic.env")
# Compare each key
for key in expected_keys:
var py_val = String(py_result[key])
var mojo_val = mojo_result[key]
if py_val != mojo_val:
raise Error("Mismatch on " + key)
This interop testing became my continuous validation throughout development. If python-dotenv returned X, mojo-dotenv had to return X.
Adding Quotes, Comments, and Whitespace Handling
Added support for:
- Single and double quotes:
QUOTED="value",SINGLE='value' - Comments: Lines starting with
# - Whitespace trimming:
KEY = value→KEY=value
Each addition came with new test fixtures and Python comparison. At this stage: I achieved around 95%+ compatibility achieved for basic use cases.
Advanced Features: Variable Expansion and Multiline Values
This is where things got interesting. python-dotenv supports:
1. Variable expansion:
BASE_DIR=/app
LOG_DIR=${BASE_DIR}/logs # Expands to /app/logs
Implementation challenge: Need two-pass parsing. The first pass extracts all variables, second pass expands references. Also need to fallback to system environment if variable not found in .env.
2. Multiline values:
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAAS
-----END PRIVATE KEY-----"
Implementation challenge: Track quote state across lines. A closing quote is only valid if preceded by an even number of backslashes (odd means the quote itself is escaped).
3. Escape sequences:
MESSAGE="Line 1\nLine 2\tTabbed"
Implementation challenge: Process escape sequences (\n, \t, \\, \", \') but only within quoted strings. Unquoted values keep backslashes literal.
4. Export prefix:
export DATABASE_URL=postgresql://localhost
Implementation challenge: Strip the export prefix while preserving everything else.
5. Inline comments:
PORT=8080 # web server
QUOTED="value # not a comment" # this IS a comment
Implementation challenge: Track quote state character-by-character to know when # starts a comment vs is part of the value.
The Tricky Bit: Multiline Quote Detection
The trickiest part? Detecting when a quoted string closes across multiple lines. Consider:
ESCAPED="Has backslash: \\"
MULTILINE="Line 1
Line 2"
Both end with ", but only MULTILINE should span lines. The difference? The number of consecutive backslashes before the quote.
Solution: Count backwards from the quote. Even count (including zero) means quote is NOT escaped:
fn count_trailing_backslashes(text: String, end_pos: Int) -> Int:
"""Count consecutive backslashes before position."""
var count = 0
var idx = end_pos - 1
while idx >= 0 and text[idx] == "\\":
count += 1
idx -= 1
return count
# In multiline parser:
if count_trailing_backslashes(line, len(line) - 1) % 2 == 0:
# Quote is NOT escaped, string is closed
Create pattern—extracting helpers for complex logic—made the codebase maintainable.
Code Review: Finding the Hidden Bugs
Before releasing v0.2.0, I did a systematic code review. The result: Reduced duplication by ~30 lines, improved readability, and found some bugs that would've surfaced in production.
Outcome: A Production-Ready Library
Final feature set (v0.2.0):
- ✅ All basic parsing (
KEY=value, quotes, comments, whitespace) - ✅ Variable expansion (
${VAR}and$VARsyntax) - ✅ Multiline values with proper quote handling
- ✅ Escape sequences in quoted strings
- ✅ Export prefix support
- ✅ Inline comments (quote-aware)
- ✅
find_dotenv()— Auto-discovery searching parent directories - ✅ Verbose mode for debugging
- ✅ Keys without
=(returns empty string) - ✅ Estimated 98%+ compatibility with
python-dotenv
Known differences (by design):
- Keys without
=return""instead ofNone(MojoDictlimitation) - Stream input not supported (file paths only)
- UTF-8 only (Mojo default)
Test coverage: 10 test files, 42 test cases using Mojo's TestSuite framework, including Python interop validation.
Distribution: Submitted to modular-community for official package distribution via pixi.
Key Lessons for Building Mojo Libraries
1. Don't Reinvent—Adapt
There's a mature Python ecosystem. For libraries like configuration management, database drivers, or logging, the hard work of discovering edge cases is done. Your job: implement in Mojo, validate compatibility.
2. Python Interop is Your Testing Superpower
Mojo's Python interop isn't just for migration, it's a testing tool. You can validate Mojo behaviour against Python in the same test file:
var py_result = Python.import_module("dotenv").dotenv_values("test.env")
var mojo_result = dotenv_values("test.env")
// Assert they match
This continuous validation caught subtle differences (like how inline comments interact with quotes) that would've been painful to debug in production.
3. Progressive Implementation Beats Big Bang
Start with the 80% use case, validate it thoroughly, then add complexity. The progression:
- Basic
KEY=valueparsing - Quotes and comments
- More advanced features (variables, multiline, escapes)
- Polishing (verbose mode,
find_dotenv, edge cases)
Each step was validated before moving forward.
4. Systematic Code Review Finds Real Bugs
For production libraries, this review step isn't optional.
5. Test What You Refactor
After refactoring to add helper functions, I added test_helpers.mojo specifically to test:
- The backslash-counting logic
- Variable lookup with system env fallback
- Edge cases like empty files and comment-only files
These tests validated the refactoring didn't change behaviour, critical for a library others hopefully depend on.
Real-World Impact: Configuration at Scale
For businesses moving production workloads to Mojo, configuration management isn't optional. You need:
- Secrets rotation without redeployment
- Environment-specific config (dev/staging/prod)
- Developer onboarding with familiar patterns
- Audit trails (verbose mode logs what's loaded)
mojo-dotenv provides all this whilst maintaining python-dotenv compatibility. Your ops team doesn't need to learn new patterns; your Mojo services just work with a familiar pattern.
Try It Yourself
Easiest: Official Package (Coming Soon)
mojo-dotenv has been submitted to modular-community, the official Modular package channel. Once merged:
pixi add `mojo-dotenv`
Current: Git Submodule
# Add to your project via git submodule
git submodule add https://github.com/databooth/mojo-dotenv vendor/mojo-dotenv
# Use in your code
mojo -I vendor/mojo-dotenv/src your_app.mojo
Or clone and explore:
git clone https://github.com/databooth/mojo-dotenv
cd mojo-dotenv
pixi install
pixi run test-all
Full example (just create your .env file with sample values):
from dotenv import load_dotenv, dotenv_values, find_dotenv
from os import getenv
fn main() raises:
# Auto-discover .env file
var env_path = find_dotenv()
# Load into environment (respects existing vars)
_ = load_dotenv(env_path, override=False, verbose=True)
# Or parse without modifying environment
var config = dotenv_values(".env.production")
var db_url = config["DATABASE_URL"]
What's Next?
v0.3.0 Roadmap:
- Multiple .env files with precedence (
.env+.env.local) - Potentially stream input support
- Enhanced error messages with line numbers
Want to contribute? The project follows standard Git workflow. Issues and PRs are most welcome!
A Note on AI-Assisted Development
I built mojo-dotenv intentionally using Warp, an AI-powered development environment with agents trained on the Modular repository. This wasn't about automating the work, it was about augmenting the development process.
How Warp helped:
- Navigating unfamiliar APIs — Mojo's
stdlibevolves rapidly (although less so as version 1.0 approaches). Warp's context about Modular's codebase meant instant answers about current APIs vs deprecated ones. - Accelerating boilerplate — Test file setup,
pixitask configuration, GitHub Actions YAML: Warp generated these more boilerplate whilst I focused on the interesting problems (multiline parsing, variable expansion). - Catching edge cases — During code review, Warp suggested additional test scenarios I hadn't considered, like comment-only files and inline comments within quotes.
- Documentation consistency — When updating multiple docs (README, DISTRIBUTION.md, submission guides), Warp ensured terminology and version numbers stayed aligned and current.
What Warp didn't do: Make architectural decisions, determine the compatibility target (python-dotenv), or design the progressive implementation strategy. Those required domain expertise and business context.
The result: mojo-dotenv went from concept to production-ready in just a couple of days, not weeks. The AI handled the mechanical aspects whilst I focused on the hard problems—edge case handling, testing strategy, ensuring production reliability and, most importantly, scrutinising and challenging the outputs.
For businesses adopting Mojo, this matters. Your team can move faster on foundational libraries, spending time on your actual competitive advantage, not reinventing configuration management or wrestling with API documentation.
Closing Thoughts
Building mojo-dotenv reinforced why Mojo is compelling for production systems:
- Performance when you need it (compiled, no interpreter overhead)
- Compatibility when you want it (Python interop for testing, familiar APIs)
- Safety when it matters (ownership tracking prevents entire classes of bugs)
For medium-sized businesses evaluating Mojo, configuration management might seem mundane. But it's precisely these foundational libraries, the ones you don't think about until they break, that enable confident production deployments.
The approach adopted here, a reference implementation in Python, progressive Mojo implementation, continuous validation, will generalise to other libraries. Database drivers, HTTP clients, logging frameworks. Stand on Python's shoulders, bring the ecosystem to Mojo incrementally, as and when warranted, validate rigorously.
If you're running Python workloads and considering Mojo, mojo-dotenv is one less item to rebuild. Focus your migration effort on your actual business logic, not reinventing configuration management.
Links:
Building production-ready data systems for medium-sized businesses. Need help evaluating Mojo for your infrastructure? Let's talk.