Flexible Arguments¶
Implementation status: Tier 0 (positional-only
/and keyword-only*) is fully implemented. Tiers 1 and 2 (@kwargsand@dynamic_kwargs) have been dropped from the roadmap.
This document specifies Sharpy's extended argument handling features, which provide Python-style parameter flexibility while maintaining static type safety.
Overview¶
Sharpy provides compile-time argument flexibility via positional-only and keyword-only parameter markers:
| Feature | Cost | Decorator Required |
|---|---|---|
Positional-only (/) and keyword-only (*) markers |
Zero (compile-time only) | No |
The vanilla C# calling convention is always preserved. For bundling optional keyword arguments, use a user-defined options struct — no special decorator needed.
Design note:
@kwargs(compiler-generated structs) and@dynamic_kwargs(dictionary-based**kwargs) were evaluated and dropped. Compiler-understood transforming decorators violate the "no magic" principle, and dynamic kwargs conflicts with Axiom 3 (type safety). Named arguments with default values and explicit option structs achieve the same goals without invisible code generation.
Tier 0: Positional-Only and Keyword-Only Parameters¶
Sharpy supports Python 3.8+ style parameter markers that enforce how arguments must be passed at call sites. This is purely compile-time validation with zero runtime cost.
Syntax¶
def example(pos_only: int, /, normal: int, *, kw_only: int) -> int:
return pos_only + normal + kw_only
/— Parameters before this marker are positional-only*— Parameters after this marker are keyword-only- Parameters between
/and*can be passed either way
Rules¶
/must appear before*if both are present/and*are not parameters themselves; they are markers- Default values work normally with both marker types
*args(variadic) implicitly acts as the*marker for keyword-only separation
Examples¶
# Positional-only parameters (before /)
def set_position(x: int, y: int, /) -> None:
"""x and y must be passed positionally."""
pass
set_position(10, 20) # ✅ Valid
set_position(x=10, y=20) # ❌ ERROR: 'x' is positional-only
set_position(10, y=20) # ❌ ERROR: 'y' is positional-only
# Keyword-only parameters (after *)
def configure(*, host: str, port: int = 8080) -> None:
"""host and port must be passed by name."""
pass
configure(host="localhost") # ✅ Valid
configure(host="localhost", port=9000) # ✅ Valid
configure("localhost") # ❌ ERROR: 'host' is keyword-only
configure("localhost", 9000) # ❌ ERROR: positional args not allowed
# Combined: positional-only, normal, and keyword-only
def search(query: str, /, limit: int = 10, *, case_sensitive: bool = False) -> list[str]:
"""
query: must be positional
limit: can be positional or keyword
case_sensitive: must be keyword
"""
pass
search("hello") # ✅ Valid
search("hello", 20) # ✅ Valid
search("hello", limit=20) # ✅ Valid
search("hello", 20, case_sensitive=True) # ✅ Valid
search(query="hello") # ❌ ERROR: 'query' is positional-only
search("hello", 20, True) # ❌ ERROR: 'case_sensitive' is keyword-only
Use Cases¶
Positional-only (/) is useful when:
- Parameter names are implementation details that shouldn't be part of the API
- You want freedom to rename parameters without breaking callers
- The parameter name would be confusing as a keyword (e.g., len(obj) not len(obj=x))
Keyword-only (*) is useful when:
- Parameters are optional flags that benefit from explicit naming
- Argument order would be confusing or error-prone
- You want to force callers to be explicit about their intent
C# Mapping¶
The generated C# signature is unchanged—markers are purely compile-time Sharpy semantics:
The Sharpy compiler tracks parameter categories and validates call sites accordingly.
Implementation: 🔄 Lowered - Compile-time validation only, no runtime cost.
~~Tier 1: Typed Kwargs (@kwargs)~~ — Dropped¶
Dropped from roadmap. See SRP-0001 for full rationale. Compiler-understood transforming decorators (generating invisible structs and overloads) violate the "no magic" principle. Named arguments with default values provide equivalent ergonomics. For reusable option bundles, define an explicit struct:
~~Tier 2: Dynamic Kwargs (@dynamic_kwargs)~~ — Dropped¶
Dropped from roadmap. See SRP-0002 for full rationale. Dynamic kwargs conflicts with Axiom 3 (type safety) and introduces a
**kwargsparameter syntax that only works with a specific decorator. For dynamic argument forwarding, pass adict[str, T]explicitly:
Interaction with Other Features¶
Pipe Operator¶
Positional-only and keyword-only markers work with the pipe operator:
def transform(data: list[int], /, *, scale: float = 1.0) -> list[float]:
return [x * scale for x in data]
result = [1, 2, 3] |> transform(scale=2.0)
Partial Application¶
Partial application works with positional-only and keyword-only validation:
def example(a: int, /, b: int, *, c: int) -> int:
return a + b + c
partial_fn = example(1, _, c=3) # ✅ 'a' positional, 'c' keyword-only
result = partial_fn(2) # Returns 6
Grammar Extensions¶
(* Parameter markers *)
parameter_list = [positional_only_params] [regular_params] [keyword_only_params] [variadic_params] ;
positional_only_params = param_def {"," param_def} "," "/" ;
regular_params = param_def {"," param_def} ;
keyword_only_params = "*" "," param_def {"," param_def} ;
variadic_params = "*" IDENTIFIER [":" type_annotation] ;
Migration Guide¶
From Python¶
| Python | Sharpy |
|---|---|
def f(x, /, y, *, z): ... |
def f(x: T, /, y: T, *, z: T) -> R: ... (same syntax, needs types) |
def f(**kwargs): ... |
Not supported — pass dict[str, T] explicitly |
| Untyped kwargs dict | Use named arguments with defaults, or a user-defined options struct |
From C¶
| C# | Sharpy |
|---|---|
| Named arguments | Same: f(name: value) |
| Optional parameters | Same: def f(x: int = 0) |
params array |
*args: T syntax |
| No equivalent | / and * markers (Sharpy-only validation) |
.NET Interop and Metadata¶
Flexible argument constraints are preserved in compiled assemblies via .NET custom attributes. This enables Sharpy code to enforce positional-only and keyword-only rules when importing functions from compiled Sharpy libraries.
Attribute Schema¶
The compiler emits a [FlexibleArgs] attribute on methods with / or * markers:
[FlexibleArgs(positionalOnlyBoundary: 0, keywordOnlyBoundary: 2)]
public static List<string> Search(string query, int limit = 10, bool caseSensitive = false)
The boundary indices indicate:
- positionalOnlyBoundary: Parameters at indices 0 through this value (inclusive) are positional-only. -1 means no positional-only parameters.
- keywordOnlyBoundary: Parameters at this index and above are keyword-only. -1 means no keyword-only parameters.
Cross-Library Enforcement¶
When Sharpy code imports a function from a compiled .dll:
# library.dll was compiled from Sharpy with:
# def search(query: str, /, limit: int = 10, *, case_sensitive: bool = False) -> list[str]
from library import search
search("hello", case_sensitive=True) # ✅ Valid
search(query="hello") # ❌ ERROR: 'query' is positional-only
The Sharpy compiler reads the [FlexibleArgs] attribute during assembly discovery and enforces constraints at compile time.
C# Callers¶
C# code calling Sharpy libraries will not automatically enforce these constraints—C# doesn't understand [FlexibleArgs]. The constraints are Sharpy-specific compile-time checks.
C# developers can: 1. Observe the attribute in IDE tooltips/documentation 2. Use a Sharpy Roslyn analyzer (if provided) for enforcement 3. Simply follow the documented API contract
Optional Per-Parameter Attributes¶
For enhanced IDE support, the compiler can optionally emit per-parameter attributes:
[FlexibleArgs(positionalOnlyBoundary: 0, keywordOnlyBoundary: 2)]
public static List<string> Search(
[PositionalOnly] string query,
int limit = 10,
[KeywordOnly] bool caseSensitive = false)
This is controlled by a compiler flag and is not emitted by default. The method-level [FlexibleArgs] attribute is the canonical source of truth.
Implementation: The FlexibleArgsAttribute, PositionalOnlyAttribute, and KeywordOnlyAttribute types are defined in Sharpy.Attributes namespace.
See Also¶
- Function Parameters — Base parameter handling
- Function Default Parameters — Default value rules
- Function Variadic Arguments —
*argshandling - Decorators — Decorator syntax and built-in decorators
- Spread Operator —
*and**unpacking syntax