Refactoring & Code Transformations¶
BetterPy provides 8 refactoring intentions and actions — from populating function arguments to extracting protocols and converting between code patterns.
Arguments¶
Make parameter optional¶
Convert a required function parameter into an optional parameter without manually editing the signature. BetterPy adds a default value at the declaration site and leaves the rest of the signature intact.
How to invoke¶
Place the caret on a required parameter and use ⌥⏎ / Alt+Enter -> "BetterPy: Make optional".
Example¶
becomes:
What it supports¶
- Function and method parameters that currently have no default value.
- Typed and untyped parameters.
- Parameters in regular Python source files in your project.
- Existing annotations and surrounding formatting.
What it deliberately avoids¶
- Parameters that already have a default value.
- Parameters whose position cannot accept a default without changing Python's required-parameter ordering rules.
*args,**kwargs, and synthetic parameters.- Files outside your own project sources.
Notes¶
- The inserted default is intentionally conservative. Review it after applying the intention when the domain needs a different sentinel or value.
- The feature is gated by the Make parameter optional setting under Settings | BetterPy | Refactoring & code transformations | Arguments.
Make parameter mandatory¶
Convert an optional function parameter back into a required parameter. BetterPy removes the default value from the signature so callers must provide an argument explicitly.
How to invoke¶
Place the caret on an optional parameter and use ⌥⏎ / Alt+Enter -> "BetterPy: Make mandatory".
Example¶
becomes:
What it supports¶
- Function and method parameters with simple default values.
- Typed and untyped parameters.
- Regular project Python files.
- Keeping annotations, decorators, and the surrounding function body unchanged.
What it deliberately avoids¶
- Parameters without a default value.
*args,**kwargs, and generated parameters.- Changes to call sites. After applying the intention, inspect callers and update any that relied on the removed default.
- Files outside your own project sources.
Notes¶
- This is a signature-editing intention, not a full call-site migration.
- The feature is gated by the Make parameter mandatory setting under Settings | BetterPy | Refactoring & code transformations | Arguments.
Top-level extract function¶
Extracts selected module-level Python statements into a new function while keeping the code explicit about inputs and outputs.
Before¶
After¶
def calculate_discounted_total(subtotal, tax, discount):
raw_total = subtotal + tax
discounted_total = raw_total - discount
return discounted_total
discounted_total = calculate_discounted_total(subtotal, tax, discount)
print(discounted_total)
The action is available through PyCharm's extract method refactoring when the current selection is at module level and can be represented as a standalone function.
Parameter Object¶
Parameter object refactoring¶
Introduce Parameter Object turns a long parameter list into a single object parameter. Inline Parameter Object performs the reverse transformation when the object no longer carries enough value to stay separate.
How to invoke¶
- Place the caret inside a function and use Refactor | BetterPy: Introduce Parameter Object....
- Place the caret on a function that takes an inlineable parameter object and use Refactor | BetterPy: Inline Parameter Object....
- When the gutter feature is enabled, a parameter-object gutter icon can start the introduce flow on eligible functions.
Introduce example¶
can become:
from dataclasses import dataclass
@dataclass
class CreateOrderParams:
customer_id: int
product_id: str
quantity: int
urgent: bool
def create_order(params: CreateOrderParams):
...
Call sites are rewritten to construct the parameter object:
Inline example¶
@dataclass
class SearchParams:
query: str
limit: int
def search(params: SearchParams):
return client.search(params.query, params.limit)
can become:
What it supports¶
- Dataclass-backed parameter objects by default.
- Alternate parameter-object base types configured in Settings | BetterPy | Parameter Object Refactoring.
- Rewriting function bodies and known call sites.
- Preserving type annotations when the source signature contains them.
- Inline refactoring for simple object-field access patterns.
What it deliberately avoids¶
- Functions whose parameters or usages cannot be rewritten predictably.
- Highly dynamic call sites where argument mapping is ambiguous.
- Parameter objects that are not recognized as a supported shape.
- Blindly deleting object classes when usages remain outside the refactoring scope.
Notes¶
- The feature is incubating. Preview the refactoring and review the changed files before committing the result.
- The feature is gated by the Parameter object refactoring setting under Settings | BetterPy | Refactoring & code transformations | Parameter Object.
Parameter object MCP tool¶
The Parameter object MCP tool exposes BetterPy's parameter-object refactoring flow to MCP-capable assistants. It lets an assistant request the same IDE-backed transformation instead of trying to rewrite Python source text on its own.
How to use¶
Connect an MCP client to the IDE, then ask it to introduce or inline a parameter object for a specific function. The tool runs inside the IDE context, so it can use project PSI, inspections, and refactoring infrastructure.
Example request¶
Introduce a parameter object for create_order in orders/service.py.
Use a dataclass and update call sites.
The assistant can route that request to BetterPy's MCP tool. The IDE performs the refactoring and returns the result through the MCP session.
What it supports¶
- Assistant-triggered introduce parameter object operations.
- The same target validation used by the interactive refactoring.
- Project-aware source edits rather than standalone text replacement.
- Respecting the parameter-object feature settings configured in the IDE.
What it deliberately avoids¶
- Running when the Parameter object refactoring feature is disabled.
- Applying transformations to unsupported or ambiguous function signatures.
- Replacing human review. You should still inspect the produced diff.
Notes¶
- This feature is useful when you want an assistant to orchestrate refactoring while the IDE remains responsible for correctness-sensitive edits.
- The feature is gated by the Parameter object MCP tool setting under Settings | BetterPy | Refactoring & code transformations | Parameter Object.
Code Transformations¶
Change visibility¶
Toggle a Python symbol between public and private naming without manually renaming the declaration and every call site. BetterPy reuses the IntelliJ rename refactoring, so all references in the project are updated together with the declaration.
How to invoke¶
Place the caret on the name of a function, class, or top-level/class-level variable, then ⌥⏎ / Alt+Enter → "Change visibility: make private" or "Change visibility: make public".
The intention text adapts to the current name:
- A name without a leading underscore offers "make private" and renames name → _name.
- A name starting with _ offers "make public" and strips leading underscores (_name → name, __name → name).
Example¶
What it supports¶
- Functions, classes, and assignment targets (module-level and class-level attributes).
- Updates every reference in the project via the standard rename refactoring, so imports and qualified usages stay in sync.
- Works on names with one or more leading underscores; making a name public removes all of them.
- When invoked on a method that is also defined in a super class or overridden in subclasses, BetterPy asks before changing the rest of the hierarchy. The dialog lists the affected super classes and subclasses and lets you choose between "Rename in Hierarchy", "Rename Only This Method", or "Cancel".
What it deliberately avoids¶
- Dunder names such as
__init__or__call__— these have language-defined semantics. - Pytest test functions (
test_*) and test classes (Test*), since renaming them would hide them from collection. - Symbols defined in
conftest.py, where the_prefix would change pytest fixture discovery. - Names whose target form would collide with a Python reserved keyword.
- Files outside your own project (library or stub sources).
Notes¶
- The intention is gated by the Change visibility setting under Settings | BetterPy | Refactoring & code transformations | Code Transformations and is enabled by default.
- Because the rename runs through
RenameProcessor, conflicts (for example an existing symbol with the target name) are surfaced through the standard refactoring conflict dialog rather than silently overwriting code.
Dict access conversion¶
This feature bundles three related intentions that help you switch between the most common dictionary lookup styles depending on whether you want terse code, explicit fallback handling, or exception-driven control flow.
All three intentions only activate for expressions BetterPy can verify as mappings. That keeps the feature from appearing on unrelated subscription syntax such as lists, tuples, or arbitrary objects that just happen to define a get() method.
Convert bracket access to .get()¶
How to invoke: ⌥⏎ / Alt+Enter on mapping[key] → "BetterPy: Replace 'dict[key]' with 'dict.get(key)'"
Use this when a missing key should quietly yield None instead of raising KeyError.
becomes:
What it supports¶
- Ordinary dictionary and mapping lookups such as
payload["id"]. - Variable keys like
payload[key_name]. - Tuple keys, including the comma form
mapping[a, b], which is rewritten tomapping.get((a, b)). - Complex mapping expressions such as
factory().cache["users"]. - Inline comments and spacing inside the brackets; BetterPy preserves the bracket contents instead of rebuilding them from scratch.
What it deliberately avoids¶
- Assignment targets like
mapping[key] = value. - Deletions such as
del mapping[key]. - Augmented assignments like
mapping[key] += 1. - Non-mapping operands such as lists.
Those cases are skipped because .get() changes the meaning: it reads a value, but it cannot stand in for write/delete syntax.
Convert .get() to bracket access¶
How to invoke: ⌥⏎ / Alt+Enter on mapping.get(key) → "BetterPy: Replace 'dict.get(key)' with 'dict[key]'"
Use this when a key is expected to exist and surfacing KeyError is preferable to silently returning None.
becomes:
What it supports¶
- One-argument
.get()calls. - Positional keys such as
mapping.get(key). - Keyword style calls like
mapping.get(key=name), which becomemapping[name].
What it deliberately avoids¶
.get()calls with a default, for examplemapping.get("timeout", 30).- Calls with zero arguments or more than one effective argument.
- Calls on non-mapping values.
- Shadowed or unrelated
getcall sites where BetterPy cannot prove the qualifier is a mapping lookup.
This restriction matters because mapping.get(key, default) is not equivalent to mapping[key]; replacing it would drop the fallback value.
Convert .get(..., default) to try / except KeyError¶
How to invoke: ⌥⏎ / Alt+Enter on mapping.get(key, default) used as a full assignment or return value → "BetterPy: Replace 'dict.get(key, default)' with try/except KeyError"
Use this when you want explicit exception-based control flow while keeping the same fallback result.
becomes:
It also works for direct returns:
becomes:
What it supports¶
- Full assignment statements with exactly one target.
- Full return statements.
- Positional and keyword forms for
keyanddefault. - Safe default expressions such as literals, constants, references, and simple constant-style subscriptions.
What it deliberately avoids¶
- Nested or partial expressions like
foo(payload.get("name", "unknown")). - Assignments with multiple targets.
- Calls without a default value.
- Defaults that may have side effects or are expensive to duplicate.
The generated try block repeats the lookup once and the fallback once, so BetterPy only offers the intention when that duplication is predictable and safe.
Convert try / except KeyError to .get()¶
How to invoke: ⌥⏎ / Alt+Enter inside a qualifying try block → "BetterPy: Replace try-except with dict.get"
Use this when explicit exception handling is unnecessary and a compact lookup reads better.
becomes:
It also supports the common initialization-plus-pass pattern:
becomes:
What it supports¶
- A single
except KeyErrorclause. - A
tryblock containing exactly one statement. - Assignment or return forms.
- Fallbacks expressed as a literal or simple reference.
- Tuple keys and parenthesized complex mapping expressions.
What it deliberately avoids¶
trystatements withelseorfinallyblocks.- Multiple
exceptclauses. - Nested subscriptions such as
mapping[a][b], where.get()would not preserve the same failure boundary. - Complex fallback expressions.
These constraints ensure the shorter .get() form stays semantically equivalent to the original exception-handling code.
When to prefer each form¶
- Use
mapping[key]when the key is required and a missing value should fail loudly. - Use
mapping.get(key)when absence is expected andNoneis a meaningful signal. - Use
mapping.get(key, default)when you want a compact fallback. - Use
try/except KeyErrorwhen the fallback path needs to be visually explicit or is about to grow into more than a single expression.
Because BetterPy supports moving in both directions, you can start with the clearest form for the current situation and change it later without rewriting the statement by hand.
Toggle enum member value¶
Toggle an enum member reference between the enum member object and its literal value. This is useful when a call site or assertion needs to switch between comparing enum members and comparing their serialized values.
How to invoke¶
Place the caret on an enum member reference or on a literal value with an expected enum type, then use ⌥⏎ / Alt+Enter -> "BetterPy: Toggle enum member / value".
Example¶
becomes:
Running the intention on "red" changes it back to Color.RED when the expression has an expected Color type, such as an annotated assignment or annotated function parameter.
What it supports¶
- Members of
Enum,IntEnum,StrEnum,Flag, andIntFlagclasses. - Qualified enum member references such as
Color.RED. - Literal values whose expected type resolves to an enum class.
- Enum references used inside expressions, assignments, and comparisons.
What it deliberately avoids¶
- Bare names such as
REDwhere the enum class is not part of the reference. - Literal values without an expected enum type annotation.
- Attributes on non-enum classes.
- Longer attribute chains such as
Color.RED.nameorColor.RED.custom. - Files outside your own project sources.
Notes¶
- The intention changes call-site references only. It does not rewrite enum member declarations.
- The feature is gated by the Toggle enum member value setting under Settings | BetterPy | Refactoring & code transformations | Type Editing.