TOML (Tom's Obvious, Minimal Language) and INI (Initialization) are human-readable configuration file
formats widely used in modern applications. This guide covers standards for consistent and maintainable
configuration files.
# Configuration file header comment# This is the main application configuration# Simple key-value pairstitle="My Application"version="1.0.0"debug=false# Numbersport=8080timeout_seconds=30pi=3.14159# Dates and times (RFC 3339)created_at=2024-01-15T10:30:00Zrelease_date=2024-01-15
# Integerscount=42negative=-17large=1_000_000# Different baseshex=0xDEADBEEFoctal=0o755binary=0b11010110# Floatspi=3.14159scientific=6.022e23negative_exp=1e-10# Special floatsinfinity=infneg_infinity=-infnot_a_number=nan
# Offset date-time (RFC 3339)created_at=2024-01-15T10:30:00Zupdated_at=2024-01-15T10:30:00-05:00# Local date-time (no timezone)scheduled=2024-01-15T10:30:00# Local daterelease_date=2024-01-15# Local timedaily_backup=03:00:00
# Simple inline tablespoint={x=1,y=2}person={name="John",age=30}# Inline tables in arrayscoordinates=[{x=0,y=0},{x=1,y=1},{x=2,y=4},]# When to use inline tables# Good - small, simple structuresdimensions={width=100,height=200}# Bad - complex structures (use standard tables instead)# user = { name = "John", address = { street = "123 Main", city = "NYC", zip = "10001" } }
; Configuration file header comment; This is the main application configuration[application]name=My Applicationversion=1.0.0debug=false[server]host=localhostport=8080timeout=30
; Semicolon comments (traditional); This is a section for server configuration# Hash comments (also widely supported)# Modern parsers support both styles[server]host=localhost; Inline comment with semicolonport=8080# Inline comment with hash
; Standard key-value pairs[database]host=localhostport=5432name=mydb; With spaces around equals (preferred for readability)[logging]level=infoformat=jsonoutput=stdout; Without spaces (also valid but less readable)[cache]enabled=truettl=3600
; Good - Clear, descriptive section names[database]host=localhost[database.primary]host=primary.example.com[database.replica]host=replica.example.com; Good - Hierarchical naming with dots[logging.console]enabled=truelevel=debug[logging.file]enabled=truepath=/var/log/app.log
; Common boolean representations[features]; Recommended: lowercase true/falseenabled=truedisabled=false; Also common: yes/nouse_cache=yesdebug_mode=no; Also seen: 1/0feature_flag=1legacy_mode=0; Also seen: on/offssl=oncompression=off
; Method 1: Continuation with indentation[messages]welcome=Hello and welcome to our application.This message spans multiple lines.Each continuation line is indented.; Method 2: Line continuation with backslash[paths]include=/usr/local/include:\
/usr/include:\
/opt/include; Method 3: Numbered keys for lists[allowed_hosts]host1=localhosthost2=example.comhost3=api.example.com
; Escaping in INI files varies by parser[paths]; Backslashes in pathswindows_path=C:\\Users\\Admin\\Documentsunix_path=/home/user/documents; Quotes for strings with special charactersmessage="Hello, World!"query="SELECT * FROM users WHERE name = 'John'"; Semicolons in values (must be quoted or escaped)connection_string="host=localhost;port=5432;database=mydb"
[package]name="my-crate"version="0.1.0"edition="2021"authors=["John Doe <john@example.com>"]description="A sample Rust crate"documentation="https://docs.rs/my-crate"homepage="https://github.com/example/my-crate"repository="https://github.com/example/my-crate"license="MIT OR Apache-2.0"keywords=["sample","crate"]categories=["development-tools"]readme="README.md"[lib]name="my_crate"path="src/lib.rs"[[bin]]name="my-cli"path="src/main.rs"[dependencies]serde={version="1.0",features=["derive"]}tokio={version="1.0",features=["full"]}anyhow="1.0"clap={version="4.0",features=["derive"]}[dev-dependencies]criterion="0.5"mockall="0.12"tempfile="3.0"[build-dependencies]cc="1.0"[features]default=["std"]std=[]async=["tokio"][profile.release]lto=truecodegen-units=1panic="abort"[profile.dev]opt-level=0debug=true[workspace]members=["crates/*"]resolver="2"
# Validate INI with Python
python-c"import configparser; c = configparser.ConfigParser(); c.read('config.ini')"# Install crudini (command-line INI parser)# On Ubuntu/Debian
apt-getinstallcrudini
# Validate INI structure
crudini--getconfig.ini
# Get specific value
crudini--getconfig.inisectionkey
# tests/test_config.pyimporttomlkitimportconfigparserimportpytestfrompathlibimportPathclassTestTomlConfig:"""Test TOML configuration files."""deftest_toml_syntax_valid(self):"""Verify TOML file parses without errors."""config_path=Path("config.toml")content=config_path.read_text()# Should not raise an exceptionconfig=tomlkit.parse(content)assertconfigisnotNonedeftest_required_sections_exist(self):"""Verify all required sections are present."""config=tomlkit.parse(Path("config.toml").read_text())required_sections=["app","server","database"]forsectioninrequired_sections:assertsectioninconfig,f"Missing required section: {section}"deftest_server_port_valid(self):"""Verify server port is in valid range."""config=tomlkit.parse(Path("config.toml").read_text())port=config["server"]["port"]assert1<=port<=65535,f"Invalid port: {port}"deftest_database_pool_size(self):"""Verify database pool configuration is reasonable."""config=tomlkit.parse(Path("config.toml").read_text())pool_min=config["database"]["pool_min"]pool_max=config["database"]["pool_max"]assertpool_min>=1,"Pool min must be at least 1"assertpool_max>=pool_min,"Pool max must be >= pool min"assertpool_max<=100,"Pool max should not exceed 100"classTestIniConfig:"""Test INI configuration files."""deftest_ini_syntax_valid(self):"""Verify INI file parses without errors."""config=configparser.ConfigParser()config.read("settings.ini")assertlen(config.sections())>0deftest_required_sections_exist(self):"""Verify all required sections are present."""config=configparser.ConfigParser()config.read("settings.ini")required_sections=["database","logging"]forsectioninrequired_sections:assertsectioninconfig.sections(),f"Missing section: {section}"deftest_boolean_values_parse_correctly(self):"""Verify boolean values are correctly interpreted."""config=configparser.ConfigParser()config.read("settings.ini")# getboolean handles various formats: true/false, yes/no, 1/0, on/offdebug=config.getboolean("app","debug",fallback=False)assertisinstance(debug,bool)deftest_integer_values_valid(self):"""Verify integer values parse correctly."""config=configparser.ConfigParser()config.read("settings.ini")port=config.getint("server","port")assert1<=port<=65535@pytest.fixturedefsample_toml():"""Provide sample TOML for testing."""return"""[server]host = "localhost"port = 8080[database]pool_min = 5pool_max = 20"""deftest_toml_parsing_fixture(sample_toml):"""Test TOML parsing with fixture."""config=tomlkit.parse(sample_toml)assertconfig["server"]["host"]=="localhost"assertconfig["server"]["port"]==8080assertconfig["database"]["pool_min"]==5
# schema_validator.pyimporttomlkitfromtypingimportAnyfromdataclassesimportdataclass@dataclassclassValidationError:"""Represents a validation error."""path:strmessage:strvalue:Anydefvalidate_toml_schema(config:dict,schema:dict)->list[ValidationError]:"""Validate TOML config against schema definition."""errors=[]defvalidate_field(path:str,value:Any,field_schema:dict):"""Validate a single field against its schema."""expected_type=field_schema.get("type")# Type checkingtype_map={"string":str,"integer":int,"float":(int,float),"boolean":bool,"array":list,"table":dict,}ifexpected_typeandnotisinstance(value,type_map.get(expected_type,object)):errors.append(ValidationError(path=path,message=f"Expected {expected_type}, got {type(value).__name__}",value=value,))return# Range validation for numbersifisinstance(value,(int,float)):if"minimum"infield_schemaandvalue<field_schema["minimum"]:errors.append(ValidationError(path=path,message=f"Value {value} below minimum {field_schema['minimum']}",value=value,))if"maximum"infield_schemaandvalue>field_schema["maximum"]:errors.append(ValidationError(path=path,message=f"Value {value} above maximum {field_schema['maximum']}",value=value,))# Enum validationif"enum"infield_schemaandvaluenotinfield_schema["enum"]:errors.append(ValidationError(path=path,message=f"Value must be one of {field_schema['enum']}",value=value,))defvalidate_section(path:str,section:dict,section_schema:dict):"""Recursively validate a section."""# Check required fieldsforrequiredinsection_schema.get("required",[]):ifrequirednotinsection:errors.append(ValidationError(path=f"{path}.{required}"ifpathelserequired,message="Required field missing",value=None,))# Validate each fieldproperties=section_schema.get("properties",{})forkey,valueinsection.items():field_path=f"{path}.{key}"ifpathelsekeyifkeyinproperties:ifisinstance(value,dict):validate_section(field_path,value,properties[key])else:validate_field(field_path,value,properties[key])validate_section("",config,schema)returnerrors# Example usageif__name__=="__main__":schema={"required":["server","database"],"properties":{"server":{"type":"table","required":["host","port"],"properties":{"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},},},"database":{"type":"table","required":["host"],"properties":{"host":{"type":"string"},"pool_size":{"type":"integer","minimum":1,"maximum":100},},},},}config=tomlkit.parse(open("config.toml").read())errors=validate_toml_schema(dict(config),schema)forerrorinerrors:print(f"Error at {error.path}: {error.message}")
# Bad - Secrets in plain text[database]host="db.example.com"password="SuperSecretPassword123"# Never do this!api_key="sk-live-abc123xyz"# Exposed in version control!# Good - Environment variable references[database]host="db.example.com"password="${DB_PASSWORD}"# Reference environment variableapi_key="${API_KEY}"# Good - Separate secrets file (not committed)# secrets.toml (in .gitignore)[database]password="actual-password"# config.toml (committed)[database]host="db.example.com"# Password loaded from secrets.toml or environment
; Bad - Secrets in INI file[database]password=MySecretPassword123; Good - Reference to environment or external source[database]password=${DB_PASSWORD}; Or use a secrets manager referencepassword_ref=aws:secretsmanager:prod/db/password
# Restrict access to configuration files
chmod600config.toml# Owner read/write only
chmod640settings.ini# Owner read/write, group read
chownapp:appconfig.toml# Appropriate ownership# For sensitive configs in production
chmod400secrets.toml# Owner read-only
# Always validate configuration before useimporttomlkitfrompathlibimportPathdefload_config(path:str)->dict:"""Load and validate configuration file."""config_path=Path(path)ifnotconfig_path.exists():raiseFileNotFoundError(f"Config file not found: {path}")# Check file permissionsmode=config_path.stat().st_modeifmode&0o077:# Check if group/other have any permissionsraisePermissionError(f"Config file has insecure permissions: {oct(mode)}")content=config_path.read_text()config=tomlkit.parse(content)# Validate required fieldsrequired_fields=["app","server","database"]forfieldinrequired_fields:iffieldnotinconfig:raiseValueError(f"Missing required config section: {field}")returndict(config)
# Sanitize values that will be used in commands or queriesimportshleximportredefsanitize_config_value(value:str,allow_pattern:str=r'^[\w\-\.]+$')->str:"""Sanitize a configuration value."""ifnotre.match(allow_pattern,value):raiseValueError(f"Invalid config value: {value}")returnvaluedefuse_config_in_command(config:dict):"""Safely use config values in shell commands."""host=sanitize_config_value(config["database"]["host"])# Use shlex.quote for shell commandscmd=f"psql -h {shlex.quote(host)}"returncmd
# Bad - Deeply nested inline tablesuser={name="John",address={street="123 Main",city={name="NYC",state="NY"}}}# Good - Use standard tables for complex structures[user]name="John"[user.address]street="123 Main"[user.address.city]name="NYC"state="NY"
# Bad - Mixing dotted keys and tables inconsistently[server]host="localhost"[server.ssl]enabled=trueserver.ssl.cert="/path/to/cert"# Error: can't add to already-defined table# Good - Consistent table definitions[server]host="localhost"[server.ssl]enabled=truecert="/path/to/cert"
# Bad - Using wrong data typesport="8080"# String instead of integerenabled="true"# String instead of booleantimeout="30"# String instead of integer# Good - Correct data typesport=8080# Integerenabled=true# Booleantimeout=30# Integer
; Bad - Inconsistent formatting[database]host=localhostport=5432name=mydb; Good - Consistent spacing[database]host=localhostport=5432name=mydb
; Bad - Duplicate sections (behavior is parser-dependent)[server]host=localhost[server]port=8080; Good - Single section with all keys[server]host=localhostport=8080
; Bad - Complex nested data (INI is not designed for this)[user]name=Johnaddress.street=123 Main Staddress.city=NYCaddress.state=NY; Good - Use separate sections or switch to TOML/YAML[user]name=John[user.address]street=123 Main Stcity=NYCstate=NY