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¶
Status: Not Yet Implemented — The attribute-based metadata described in this section is planned but not yet implemented. Currently, flexible argument constraints (
/and*markers) are enforced only within Sharpy source compilation and are not preserved in compiled assemblies. The design below describes the intended future behavior.
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