Skip to content

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

Since 2026.04.05 · Maturity Incubating

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

def greet(name: str, title: str):
    return f"{title} {name}"

becomes:

def greet(name: str, title: str = ""):
    return f"{title} {name}"

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

Since 2026.04.05 · Maturity Incubating

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

def greet(name: str, title: str = ""):
    return f"{title} {name}"

becomes:

def greet(name: str, title: str):
    return f"{title} {name}"

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

Since 2026.06.0 · Maturity Incubating · Issues PY-40840PY-44636PY-73852

Extracts selected module-level Python statements into a new function while keeping the code explicit about inputs and outputs.

Before

raw_total = subtotal + tax
discounted_total = raw_total - discount
print(discounted_total)

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

Since 2026.04.05 · Maturity Incubating · Issues PY-59270

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

def create_order(customer_id: int, product_id: str, quantity: int, urgent: bool):
    ...

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:

create_order(CreateOrderParams(customer_id, product_id, quantity, urgent))

Inline example

@dataclass
class SearchParams:
    query: str
    limit: int


def search(params: SearchParams):
    return client.search(params.query, params.limit)

can become:

def search(query: str, limit: int):
    return client.search(query, limit)

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

Since 2026.04.05 · Maturity Stable

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

Since 2026.04.13 · Maturity Stable

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 (_namename, __namename).

Example

def calculate(value):
    return value * 2

print(calculate(2))
becomes:
def _calculate(value):
    return value * 2

print(_calculate(2))

class _Helper:
    pass

def build() -> _Helper:
    return _Helper()
becomes:
class Helper:
    pass

def build() -> Helper:
    return Helper()

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

Since 2026.04.13 · Maturity Incubating · Issues PY-54845

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.

timeout = config["timeout"]

becomes:

timeout = config.get("timeout")
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 to mapping.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.

username = payload.get("username")

becomes:

username = payload["username"]
What it supports
  • One-argument .get() calls.
  • Positional keys such as mapping.get(key).
  • Keyword style calls like mapping.get(key=name), which become mapping[name].
What it deliberately avoids
  • .get() calls with a default, for example mapping.get("timeout", 30).
  • Calls with zero arguments or more than one effective argument.
  • Calls on non-mapping values.
  • Shadowed or unrelated get call 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.

result = payload.get("name", "unknown")

becomes:

try:
    result = payload["name"]
except KeyError:
    result = "unknown"

It also works for direct returns:

return payload.get("name", "unknown")

becomes:

try:
    return payload["name"]
except KeyError:
    return "unknown"
What it supports
  • Full assignment statements with exactly one target.
  • Full return statements.
  • Positional and keyword forms for key and default.
  • 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.

try:
    result = payload["name"]
except KeyError:
    result = "unknown"

becomes:

result = payload.get("name", "unknown")

It also supports the common initialization-plus-pass pattern:

result = "unknown"
try:
    result = payload["name"]
except KeyError:
    pass

becomes:

result = payload.get("name", "unknown")
What it supports
  • A single except KeyError clause.
  • A try block 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
  • try statements with else or finally blocks.
  • Multiple except clauses.
  • 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 and None is a meaningful signal.
  • Use mapping.get(key, default) when you want a compact fallback.
  • Use try / except KeyError when 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

Since 2026.05.2 · Maturity Stable

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

from enum import Enum


class Color(Enum):
    RED = "red"


payload = Color.RED

becomes:

payload = "red"

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, and IntFlag classes.
  • 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 RED where 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.name or Color.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.