You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@beam.apache.org by "hjtran (via GitHub)" <gi...@apache.org> on 2026/07/14 20:43:21 UTC
[PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
hjtran opened a new pull request, #38268:
URL: https://github.com/apache/beam/pull/38268
## Summary
Adds `PCollection.with_side_outputs(**kwargs)` to the Python SDK so transforms can return a single, chainable `PCollection` that *also* carries named side-output PCollections, accessible via `result.side_outputs.<tag>`.
```python
class MyFilter(beam.PTransform):
def expand(self, pcoll):
results = pcoll | beam.ParDo(FilterDoFn()).with_outputs('dropped', main='main')
return results['main'].with_side_outputs(dropped=results['dropped'])
# Most users — just chain
inputs | MyFilter() | NextTransform()
# When you need the side output
result = inputs | MyFilter()
result.side_outputs.dropped
```
Today, transform authors face a tradeoff between returning a `PCollection` (chainable, but loses side outputs) or a `dict`/`PCollectionTuple` (preserves side outputs, but breaks chaining). This change lets them have both.
## Design
- New `_SideOutputsContainer` exposes attribute (`container.dropped`) and item (`container["dropped"]`) access plus iteration / `len` / `in`.
- `PCollection.with_side_outputs(**kwargs)` returns a `copy.copy(self)` with the side-output mapping attached. The original is unchanged. Validation: each value must be a `PCollection` (TypeError) on the same pipeline (ValueError).
- `Pipeline._apply_internal` and `Pipeline._replace` both call a shared helper that registers the side outputs on the wrapping `AppliedPTransform` — *only* for the top-level returned `PCollection`. This makes side outputs first-class in the pipeline graph (visible to runners, the proto, visualization, and YAML's `Transform.tag` resolution).
- The helper enforces two apply-time invariants:
- **Provenance:** each side output's producer must be the wrapping transform itself or one of its descendants. Foreign PCollections are rejected with `ValueError`.
- **Tag collision:** if a side-output tag already exists in the wrapping transform's outputs, the value must be the same `PCollection` object (idempotent); otherwise `ValueError`.
- `__copy__` is added to `PCollection` so `copy.copy(self)` doesn't dispatch to the existing `__reduce_ex__` anti-pickling hook.
## Chaining behavior
`pc | A | B` is unchanged. `B` receives `A`'s main output as a normal PCollection. `A`'s side outputs are not carried forward — capture the intermediate result if you need them. This matches the behavior described in the proposal and requires no special code.
## Caveats
- `**kwargs` syntax restricts tags to valid Python identifiers. Tags with hyphens/spaces are not supported here; users with arbitrary string tags should keep using the existing dict / `DoOutputsTuple` patterns.
- `.side_outputs` is a construction-time ergonomic, not a durable graph property. After `from_runner_api`, the reconstructed PCollection won't have `_side_outputs` populated, but the side outputs themselves still exist as named outputs on the producer's `AppliedPTransform`.
- Side outputs do not participate in composite-boundary type checking (matches the existing behavior of side outputs accessed via `DoOutputsTuple`).
## Follow-ups (not in this PR)
- `ParDo.with_side_outputs(...)` convenience method.
- Beam YAML dot-modifier integration (the existing `get_pcollection` already resolves `Transform.tag` by walking the producer's outputs dict, so this should mostly "just work" once verified).
## Tests
`pvalue_test.py` covers: copy semantics, attribute / index access, missing-tag errors, type/pipeline validation, empty container, double-call replace.
`pipeline_test.py` covers: end-to-end materialization with `assert_that`, the canonical `MyFilter`-style composite wrapping `with_outputs`, foreign-pcollection rejection, tag-collision rejection, idempotent same-object collision, replacement-path registration via `Pipeline.replace_all`, runner API round-trip, and the nested-return non-flattening case.
## Test plan
- [x] `cd sdks/python && python -m pytest apache_beam/pvalue_test.py apache_beam/pipeline_test.py` — 84 passed, 1 skipped
- [x] `ruff check --config=sdks/python/ruff.toml` on the four changed files — clean
- [x] `yapf --diff` on the four changed files — clean
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "hjtran (via GitHub)" <gi...@apache.org>.
hjtran commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-4981113558
Whoops sorry I didnt mean to post this yet. No need to review yet @jrmccluskey
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-4976482209
Assigning reviewers:
R: @jrmccluskey for label python.
Note: If you would like to opt out of this review, comment `assign to next reviewer`.
Available commands:
- `stop reviewer notifications` - opt out of the automated review tooling
- `remind me after tests pass` - tag the comment author after tests pass
- `waiting on author` - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)
The PR bot will only process comments in the main thread (not review comments).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] closed pull request #38268: [Python] Add PCollection.with_side_outputs for chainable side outputs
URL: https://github.com/apache/beam/pull/38268
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-5058513921
Reminder, please take a look at this pr: @jrmccluskey
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-4769293559
This pull request has been marked as stale due to 60 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@beam.apache.org list. Thank you for your contributions.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] closed pull request #38268: [Python] Add PCollection.with_side_outputs for chainable side outputs
URL: https://github.com/apache/beam/pull/38268
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-4843853853
This pull request has been closed due to lack of activity. If you think that is incorrect, or the pull request requires review, you can revive the PR at any time.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "gemini-code-assist[bot] (via GitHub)" <gi...@apache.org>.
gemini-code-assist[bot] commented on code in PR #38268:
URL: https://github.com/apache/beam/pull/38268#discussion_r3583944613
##########
sdks/python/apache_beam/pipeline.py:
##########
@@ -112,6 +112,42 @@
__all__ = ['Pipeline', 'transform_annotations']
+def _descendant_applied_ptransforms(
+ current: 'AppliedPTransform') -> set['AppliedPTransform']:
+ descendants = set()
+ pending = list(current.parts)
+ while pending:
+ part = pending.pop()
+ if part in descendants:
+ continue
+ descendants.add(part)
+ pending.extend(part.parts)
+ return descendants
+
+
+def _register_top_level_side_outputs(
+ current: 'AppliedPTransform', result: pvalue.PCollection) -> None:
+ if not result._side_outputs:
+ return
+
+ descendants = _descendant_applied_ptransforms(current)
+ for side_tag, side_pcoll in result._side_outputs.items():
Review Comment:

Accessing `result._side_outputs` directly can raise an `AttributeError` if the `PCollection` was reconstructed (e.g., via `from_runner_api` or other deserialization paths) where `_side_outputs` is not populated in the instance's `__dict__`. Using `getattr(result, '_side_outputs', None)` is safer and prevents potential crashes during pipeline replacement or other graph operations on reconstructed pipelines.
```python
side_outputs = getattr(result, '_side_outputs', None)
if not side_outputs:
return
descendants = _descendant_applied_ptransforms(current)
for side_tag, side_pcoll in side_outputs.items():
```
##########
sdks/python/apache_beam/pvalue.py:
##########
@@ -138,19 +139,105 @@ def __or__(self, ptransform):
return self.pipeline.apply(ptransform, self)
+class _SideOutputsContainer:
+ """Lightweight accessor over named side-output PCollections.
+
+ Supports attribute access (``container.dropped``), indexing
+ (``container["dropped"]``), iteration over tag names, ``len()``, and
+ ``in``. ``__getattr__`` on a missing tag raises ``AttributeError`` with
+ a message listing available tags.
+ """
+ def __init__(self, side_outputs: dict[str, 'PCollection']):
+ object.__setattr__(self, '_side_outputs', dict(side_outputs))
+
+ def __getattr__(self, tag: str) -> 'PCollection':
+ if tag.startswith('__'):
+ raise AttributeError(tag)
+ try:
+ return self._side_outputs[tag]
+ except KeyError as exc:
+ available = sorted(self._side_outputs)
+ raise AttributeError(
+ f"No side output named {tag!r}. Available: {available}") from exc
+
+ def __getitem__(self, tag: str) -> 'PCollection':
+ return self._side_outputs[tag]
+
+ def __iter__(self):
+ return iter(self._side_outputs)
+
+ def __len__(self):
+ return len(self._side_outputs)
+
+ def __contains__(self, tag):
+ return tag in self._side_outputs
+
+
class PCollection(PValue, Generic[T]):
"""A multiple values (potentially huge) container.
Dataflow users should not construct PCollection objects directly in their
pipelines.
"""
+ def __init__(
+ self,
+ pipeline: 'Pipeline',
+ tag: Optional[str] = None,
+ element_type: Optional[Union[type, 'typehints.TypeConstraint']] = None,
+ windowing: Optional['Windowing'] = None,
+ is_bounded=True):
+ super().__init__(
+ pipeline,
+ tag=tag,
+ element_type=element_type,
+ windowing=windowing,
+ is_bounded=is_bounded)
+ self._side_outputs: Optional[dict[str, 'PCollection']] = None
+
def __eq__(self, other):
if isinstance(other, PCollection):
return self.tag == other.tag and self.producer == other.producer
def __hash__(self):
return hash((self.tag, self.producer))
+ def __copy__(self):
+ result = type(self).__new__(type(self))
+ result.__dict__ = self.__dict__.copy()
+ return result
+
+ @property
+ def side_outputs(self) -> _SideOutputsContainer:
+ return _SideOutputsContainer(self._side_outputs or {})
Review Comment:

Accessing `self._side_outputs` directly in the `side_outputs` property will raise an `AttributeError` if the `PCollection` was reconstructed (e.g., from the runner API) or created without calling `__init__` (which is common in some serialization or mocking frameworks). Using `getattr(self, '_side_outputs', None)` ensures that this property safely returns an empty container instead of crashing.
```suggestion
return _SideOutputsContainer(getattr(self, '_side_outputs', None) or {})
```
##########
sdks/python/apache_beam/pvalue.py:
##########
@@ -138,19 +139,105 @@ def __or__(self, ptransform):
return self.pipeline.apply(ptransform, self)
+class _SideOutputsContainer:
+ """Lightweight accessor over named side-output PCollections.
+
+ Supports attribute access (``container.dropped``), indexing
+ (``container["dropped"]``), iteration over tag names, ``len()``, and
+ ``in``. ``__getattr__`` on a missing tag raises ``AttributeError`` with
+ a message listing available tags.
+ """
+ def __init__(self, side_outputs: dict[str, 'PCollection']):
+ object.__setattr__(self, '_side_outputs', dict(side_outputs))
+
+ def __getattr__(self, tag: str) -> 'PCollection':
+ if tag.startswith('__'):
+ raise AttributeError(tag)
Review Comment:

If `_side_outputs` is not yet initialized or is missing from `__dict__` (for example, during unpickling or copying), accessing `self._side_outputs` inside `__getattr__` will trigger `__getattr__('_side_outputs')`, leading to infinite recursion and a `RecursionError`. Restricting dynamic attribute lookup to exclude any attributes starting with an underscore (or specifically `_side_outputs`) prevents this infinite recursion.
```suggestion
if tag.startswith('_'):
raise AttributeError(tag)
```
##########
sdks/python/apache_beam/pvalue.py:
##########
@@ -138,19 +139,105 @@ def __or__(self, ptransform):
return self.pipeline.apply(ptransform, self)
+class _SideOutputsContainer:
+ """Lightweight accessor over named side-output PCollections.
+
+ Supports attribute access (``container.dropped``), indexing
+ (``container["dropped"]``), iteration over tag names, ``len()``, and
+ ``in``. ``__getattr__`` on a missing tag raises ``AttributeError`` with
+ a message listing available tags.
+ """
+ def __init__(self, side_outputs: dict[str, 'PCollection']):
+ object.__setattr__(self, '_side_outputs', dict(side_outputs))
Review Comment:

Since `_SideOutputsContainer` does not override `__setattr__`, using `object.__setattr__` is unnecessary and can be confusing. Standard attribute assignment `self._side_outputs = dict(side_outputs)` is cleaner and more idiomatic.
```suggestion
def __init__(self, side_outputs: dict[str, 'PCollection']):
self._side_outputs = dict(side_outputs)
```
##########
sdks/python/apache_beam/pvalue.py:
##########
@@ -138,19 +139,105 @@ def __or__(self, ptransform):
return self.pipeline.apply(ptransform, self)
+class _SideOutputsContainer:
+ """Lightweight accessor over named side-output PCollections.
+
+ Supports attribute access (``container.dropped``), indexing
+ (``container["dropped"]``), iteration over tag names, ``len()``, and
+ ``in``. ``__getattr__`` on a missing tag raises ``AttributeError`` with
+ a message listing available tags.
+ """
+ def __init__(self, side_outputs: dict[str, 'PCollection']):
+ object.__setattr__(self, '_side_outputs', dict(side_outputs))
+
+ def __getattr__(self, tag: str) -> 'PCollection':
+ if tag.startswith('__'):
+ raise AttributeError(tag)
+ try:
+ return self._side_outputs[tag]
+ except KeyError as exc:
+ available = sorted(self._side_outputs)
+ raise AttributeError(
+ f"No side output named {tag!r}. Available: {available}") from exc
+
+ def __getitem__(self, tag: str) -> 'PCollection':
+ return self._side_outputs[tag]
+
+ def __iter__(self):
+ return iter(self._side_outputs)
+
+ def __len__(self):
+ return len(self._side_outputs)
+
+ def __contains__(self, tag):
+ return tag in self._side_outputs
Review Comment:

Implementing `__dir__` on `_SideOutputsContainer` allows interactive environments (like Jupyter notebooks or IPython) to auto-complete the available side output tags, greatly improving usability.
```suggestion
def __contains__(self, tag):
return tag in self._side_outputs
def __dir__(self):
return sorted(super().__dir__() + list(getattr(self, '_side_outputs', {}).keys()))
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-5890696100
This pull request has been closed due to lack of activity. If you think that is incorrect, or the pull request requires review, you can revive the PR at any time.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-5776780100
This pull request has been marked as stale due to 60 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@beam.apache.org list. Thank you for your contributions.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [Python] Add PCollection.with_side_outputs for chainable side outputs [beam]
Posted by "gemini-code-assist[bot] (via GitHub)" <gi...@apache.org>.
gemini-code-assist[bot] commented on PR #38268:
URL: https://github.com/apache/beam/pull/38268#issuecomment-4298918434
## Summary of Changes
Hello, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request enhances the Apache Beam Python SDK by enabling PCollections to carry named side outputs while remaining chainable. This solves a long-standing usability issue where transform authors had to choose between returning a chainable PCollection or a container that preserves side outputs. The changes introduce a new API for attaching side outputs and ensure these are correctly registered within the pipeline graph, maintaining compatibility with existing runner APIs and composite transform structures.
### Highlights
* **New PCollection.with_side_outputs method**: Introduced a new method to PCollection that allows transforms to return a chainable PCollection while also carrying named side-output PCollections.
* **Side output access**: Added a _SideOutputsContainer to provide ergonomic attribute and item access to side outputs, such as result.side_outputs.tag.
* **Pipeline graph integration**: Updated Pipeline._apply_internal and _replace to automatically register these side outputs on the wrapping AppliedPTransform, ensuring they are visible to runners and pipeline visualization tools.
* **Validation and Safety**: Implemented strict validation to ensure side outputs belong to the same pipeline and are produced by the correct transform, preventing common graph construction errors.
🧠 **New Feature in Public Preview:** You can now enable **Memory** to help **Gemini Code Assist** learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. **Click [here](https://codeassist.google/code-review/login) to enable Memory in your admin console.**
<details>
<summary><b>Using Gemini Code Assist</b></summary>
<br>
The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.
<b>Invoking Gemini</b>
You can request assistance from Gemini at any point by creating a comment using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands on the current page.
Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help | `/gemini help` | Displays a list of available commands.
<b>Customization</b>
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).
<b>Limitations & Feedback</b>
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up [here](https://google.qualtrics.com/jfe/form/SV_2cyuGuTWsEw84yG).
</details>
[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: github-unsubscribe@beam.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org