PyExpression is a small, controlled, Python-like expression and mini-script engine for C#.
It is built for hosts that want a scripting layer without taking on a full Python runtime or exposing arbitrary .NET object access.
What it is good at:
- evaluating small user-defined rules and formulas
- running predictable mini-scripts with loops, conditions, and collections
- exposing only an approved callable surface through
PyLib - extending that callable surface through explicit plugin assemblies
What it is deliberately not:
- a Python implementation
- an embedded CPython runtime
- IronPython
- Roslyn scripting
- a bridge to arbitrary .NET objects, the file system, or the network
This repository contains four projects:
PyExpression- the core library and the only project published as a NuGet packagePyFilter.Methods- an example plugin assembly that shows how to add custom methods; it is not published by CIPyExpression.tests- xUnit tests for the engine and plugin loadingPyExpressionSandbox- a Windows-only WPF demo app for interactive experimentation
These are the main points that save people time:
- The library currently targets
net10.0. You need the .NET 10 SDK to build this repository. PyExpressionSandboxis WPF and therefore Windows-only.- The core library and tests can be built on non-Windows machines. The sandbox cannot.
- Plugins are not auto-loaded. If you want extra methods, register them explicitly or load them from a folder.
PyLib.Defaultonly contains the built-inMathMethodsprovider.yieldvalues are only exposed throughStreamAsync(...). If you callExecute(), you get the final return value, not a stream.
If the package version you want has already been published to nuget.org:
dotnet add package PyExpressionPackage details:
- Package ID:
PyExpression - Target framework:
net10.0
If the package is not available yet, build from source using the commands in Building From Source. NuGet publishing is tag-driven; normal branch pushes and pull requests do not publish packages.
using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;
var result = PyExpressionEngine.Execute(
"""
x = n * 2
return x
""",
new PyVar("n", 9));
Console.WriteLine(result.Value); // 18using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;
var expression = new PyExpressionEngine(
"""
if n > 10:
return 100
else:
return 50
""")
{
Vars = new[]
{
new PyVar("n", 11)
}
};
var result = expression.Execute();
Console.WriteLine(result.Value); // 100If you run the same script repeatedly with different variables, prepare it once and reuse it:
using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;
var expression = new PyExpressionEngine("return n * 2");
var prepared = expression.Prepare();
var first = expression.Execute(prepared, new[] { new PyVar("n", 7) });
var second = expression.Execute(prepared, new[] { new PyVar("n", 9) });
Console.WriteLine(first.Value); // 14
Console.WriteLine(second.Value); // 18using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;
var expression = new PyExpressionEngine(
"""
i = 0
while i < 3:
i = i + 1
yield i
""");
await foreach (var item in expression.StreamAsync())
{
Console.WriteLine(item);
}Supported literals:
- integers
- doubles
- booleans
- strings
Supported expressions:
+,-,*,/,%==,!=,<,<=,>,>=and,or,not
Supported statements:
- assignment
returnif/elsewhilefor ... in range(...)breakcontinueyieldyield from
Supported collections:
- list literals
- dictionary literals
- index access
- indexed assignment for lists and dictionaries
Built-in methods in PyLib.Default:
absminmaxsumsortedkeysvaluesroundfloorceilpowsqrtlenclamp
These omissions are intentional:
import- user-defined functions inside the script
- class definitions
- arbitrary reflection
- access to arbitrary .NET objects
- file system access
- network access
- process control
- dynamic assembly execution from script code
If you need additional capabilities, the expected extension point is PyLib, not unrestricted host access.
Important public types:
PyExpression.PyExpression- the main execution classPyVar- named variables passed into or returned from the enginePyLib- registry of approved callable methodsIPyMethods/PyMethods- plugin provider contract and helper base classIPyPluginManifest/PyPluginManifest- plugin metadataPluginLoadResult- per-assembly plugin load result
Common members:
PyExpression.Execute(...)PyExpression.ExecuteAsync(...)PyExpression.StreamAsync(...)PyExpression.Prepare(...)PyExpression.ContainsYield()PyLib.Register(...)PyLib.LoadPluginsFromFolder(...)
For a type-by-type reference, see docs/api-reference.md.
Plugins are controlled method provider assemblies that implement IPyMethods.
Important behavior:
- Plugins are not discovered automatically.
- The default library does not scan the application directory.
- Folder loading scans only the specified folder for
*.dll; it does not recurse into subfolders. - Folder loading instantiates provider types via
Activator.CreateInstance(...), so provider types must bepublic, non-abstract, and constructible without custom arguments.
using PyExpression;
using PyFilter.Methods;
using PyExpressionEngine = PyExpression.PyExpression;
var library = new PyLib().Register(new PyFilterMethods());
var expression = new PyExpressionEngine("return contains(\"hello world\", \"world\")")
{
Library = library
};
var result = expression.Execute();
Console.WriteLine(result.Value); // Truevar library = new PyLib().Register(typeof(PyFilter.Methods.PyFilterMethods).Assembly);var library = new PyLib();
var results = library.LoadPluginsFromFolder(@"C:\plugins");
foreach (var result in results)
{
Console.WriteLine($"{result.AssemblyName}: success={result.Success}");
}PyFilter.Methods shows a real plugin assembly and registers:
containsstarts_withends_withmatchesis_null_or_whitespace
If you are building your own plugin, start with how-to-create-a-plugin.md.
The engine uses explicit exception types so callers can distinguish between parse failures, runtime failures, missing variables, missing methods, invalid argument counts, and iteration-limit violations.
Relevant exception types:
PyParseExceptionPyRuntimeExceptionPyUnknownVariableExceptionPyUnknownMethodExceptionPyMethodArgumentExceptionPyIterationLimitExceededException
Example:
using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;
try
{
var result = PyExpressionEngine.Execute("return missing");
}
catch (PyUnknownVariableException ex)
{
Console.WriteLine(ex.Message);
}These are the things most likely to trip up first-time users:
Execute()returns the final result. It does not enumerateyieldvalues.- A script without
returncompletes successfully and returnsnull. - Assigning to an input variable updates the supplied
PyVarinstance in place. - Extra methods are unavailable until they are registered in the
Libraryused by thatPyExpressioninstance. PyExpressionSandboxfailing on macOS or Linux is expected; build the library and tests directly instead.PyFilter.Methodsis sample code. The CI publish path only packsPyExpression.
n = 5
x = n * 2
return xif n > 10:
return 100
else:
return 50i = 0
sum = 0
while i < 5:
sum = sum + i
i = i + 1
return sumsum = 0
for i in range(1, 6, 2):
sum = sum + i
return sumi = 0
while i < 3:
i = i + 1
yield iThese commands work on Windows, macOS, and Linux:
dotnet build PyExpression/PyExpression.csproj -c Release
dotnet build PyFilter.Methods/PyFilter.Methods.csproj -c Release
dotnet test PyExpression.tests/PyExpression.tests.csproj -c ReleaseIf you only want to validate the engine, the test command is usually enough because it builds the referenced projects automatically:
dotnet test PyExpression.tests/PyExpression.tests.csproj -c ReleaseThe sandbox is a WPF application and only builds on Windows:
dotnet build PyExpressionSandbox/PyExpressionSandbox.csproj -c Release
dotnet build PyExpressionSandbox.slnx -c Releasedotnet pack PyExpression/PyExpression.csproj -c Release -o ./artifacts/nugetThis packs only PyExpression, which is the same packaging boundary used by CI.
GitHub Actions behavior in this repository:
- branch pushes and pull requests run restore, build, and test
- version tags matching
v*additionally packPyExpression - trusted publishing is used to publish the resulting package to nuget.org
Examples:
v1.2.3-> stable release publishv1.2.3-rc.1-> prerelease publish
Only PyExpression is published. PyFilter.Methods remains an example plugin project inside the repository.
The test project covers:
- variable access and reassignment
- arithmetic and operator precedence
- comparisons and boolean logic
- conditions
whileandfor ... in range(...)breakandcontinue- lists, dictionaries, indexing, and indexed assignment
- built-in methods
- direct plugin registration, assembly scanning, and folder-based loading
- async execution
yieldandyield from- parse and runtime error cases
.
|-- .github/
| `-- workflows/
| `-- ci.yml
|-- PyExpression/
|-- PyExpression.tests/
|-- PyExpressionSandbox/
|-- PyExpressionSandbox.slnx
|-- PyFilter.Methods/
|-- docs/
|-- how-to-create-a-plugin.md
|-- LICENSE
`-- README.md
This project is licensed under the MIT License. See LICENSE.