Python Nodes
Python is the most common language for nodes in Bioinformatics Studio. Here’s how they work — no PhD required.
The Simplest Python Node
Here’s a complete, working Python node:
@node()
def normalize(adata, target_sum=1e4):
import scanpy as sc
sc.pp.normalize_total(adata, target_sum=target_sum)
return adataThis node has 2 inputs (adata, target_sum) and 1 output (adata).
The @node Decorator
Every Python node starts with a decorator — a small tag that tells the system “this is a workflow node, not just a regular function.”
@node()
def my_node():
...Add @node() above your function, and the system knows what to do.
What Is an Input?
Inputs are the function parameters — whatever goes inside the parentheses.
@node()
def normalize(adata, target_sum):
# ↑ ↑
# input 1 input 2This node has two inputs: adata and target_sum. When the workflow runs, the system automatically injects values from connected upstream nodes into these parameters. You don’t do anything — just name them, and the values appear.
Default Values
You can provide default values for inputs. If nothing is connected, the default is used:
@node()
def normalize(adata, target_sum=1e4):
# ↑ default valueWhat Is an Output?
Outputs come from the return statement at the end of your function.
@node()
def normalize(adata, target_sum):
sc.pp.normalize_total(adata, target_sum=target_sum)
return adata
# ↑
# output 1Multiple Outputs
Return multiple values separated by commas:
@node()
def qc_filter(adata, min_genes):
...
return adata, qc_report
# ↑ ↑
# out 1 out 2Now downstream nodes can connect to either adata or qc_report.
The Golden Rule
Always return a variable name, never an expression.
✅ Do this:
result = a + b
return result❌ Not this:
return a + bThe system reads the variable name to create the output handle. return a + b has no name — it can’t create a proper handle.
How Variables Arrive
When you connect Node A’s output to Node B’s input, the value flows automatically. Inside your function, the parameter is just… there.
@node()
def my_node(adata):
# adata already contains the AnnData object from the upstream node
print(adata.shape)
return adataNo loading, no deserialization, no input['adata'] — just use it directly.
A Complete Example
Here’s a real quality control node for single-cell data:
@node()
def run_qc(adata, min_genes=200, max_mt_percent=20):
import scanpy as sc
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)
adata = adata[adata.obs.n_genes_by_counts >= min_genes, :]
adata = adata[adata.obs.pct_counts_mt < max_mt_percent, :]
return adataReal-Time Output
Want to see things while your node runs? Use display():
@node()
def analyze(adata):
display(adata.obs.head()) # shows a table in real-time
...And plt.show() is automatically captured — just call it and figures appear in the output panel:
@node()
def plot_data(adata):
import matplotlib.pyplot as plt
plt.scatter(adata.obs['n_genes'], adata.obs['total_counts'])
plt.show() # captured automatically — no need to save to file
return adataPlain Python Scripts (No Decorator)
You can also write Python without the @node() decorator — just a plain script. In this case, UPPERCASE variable assignments at the top become inputs (same as bash and R nodes):
INPUT_FILE = "data/counts.csv"
OUTPUT_DIR = "results/"
import pandas as pd
df = pd.read_csv(INPUT_FILE)
df.to_csv(f"{OUTPUT_DIR}/processed.csv", index=False)
result = dfThis script has 2 inputs (INPUT_FILE, OUTPUT_DIR) and 1 output (result — the last assigned variable).
Use the @node() decorator when you want clean function-based nodes. Use plain scripts for quick, simple tasks.
Good to Know
Verbose output? Write to a log file
If your code produces a lot of output (e.g., training a model), write logs to a file and only print key milestones:
@node()
def train_model(adata):
import logging
logging.basicConfig(filename='training.log', level=logging.INFO)
logging.info('Started training')
# ... training loop ...
print('Training complete. See training.log for details.')
return modelNo timeout — nodes can run for hours
There’s no execution timeout. If your node runs a long alignment or training job, that’s fine — it won’t be killed.
Large objects are handled automatically
DataFrames, AnnData objects, and other non-JSON-serializable objects are automatically serialized to disk and passed by reference. You don’t need to manually save and reload — just return them.
Quick Summary
| Concept | How it works |
|---|---|
| Decorator | @node() above your function |
| Inputs | Function parameters |
| Default values | def my_node(adata, threshold=0.05): |
| Outputs | return values |
| Multiple outputs | return a, b, c |
| Golden rule | Return variable names, not expressions |
| Variable arrival | Automatic — just use the parameter name |
| Real-time display | display(value) or plt.show() |
| Plain scripts | UPPERCASE assignments become inputs (no decorator needed) |
| Verbose output | Write to a log file, print only milestones |
| No timeout | Nodes can run for hours or days |