Visitar URL original
Fix swapped values when reading BINARY votable columns out of file order by jdavies-st · Pull Request #20513 · astropy/astropy · GitHub
Skip to content

Fix swapped values when reading BINARY votable columns out of file order - #20513

Open
jdavies-st wants to merge 2 commits into
astropy:mainfrom
jdavies-st:votable-binary-column-order
Open

jdavies-st wants to merge 2 commits into
astropy:mainfrom
jdavies-st:votable-binary-column-order

Conversation

@jdavies-st

Copy link
Copy Markdown
Contributor

Description

Reading a BINARY or BINARY2 table with columns= in a different order from the file put each column's values into another column, or raised ValueError when their types differed. A column listed twice raised IndexError. The selected columns are now read once each, in file order, which is the order the result holds them in. TABLEDATA was already correct.

Fixes #20512

AI Disclosure

Claude Opus 5.5 discovered the bug when I was fixing a different issue with the BINARY reader and was used to explore other failure modes, a simple reproducer and the final 1-line fix. I did final code review.

  • I certify that I am human and that I take full responsibility for this pull request including all interactions with reviewers.

Merge method

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@jdavies-st
jdavies-st force-pushed the votable-binary-column-order branch from 30cfd38 to f4facd0 Compare October 1, 2026 07:07
)
buffer.seek(0)

array = parse_single_table(buffer, columns=["a", "a"]).array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the user really asking for when they give columns=["a", "a"]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mistake? This is a very corner case that sorta falls out with the fix. They get precisely one column back, column a. We don't need the unit test if it is bothersome - i.e. we don't want to have a unit test that codifies unlikely API requests. Before it raised an IndexError, which was not intuitive either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the more likely case is that they give pass 15 of the 96 columns names to the parameter, and accidentally repeat one of them. Now it gracefully gives them that column back once instead of IndexError.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that if I give columns=["a", "a"], I would expect duplicate columns or an error telling me that I cannot do it (if duplicate columns is against the standard). But I also don't use VOTable much.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be done. Currently columns don't come back in order that that they are listed in the columns=[...] param, and they never have. So the list is not absolute in the sense of, "produce a table of exactly these columns in this order". We've always returned them in the order they are in the table. So if I passed:

columns = ["flux", "error", "mag_a", "mag_a_error", "flag1", "flux"]

I still get back flux once, and I get them back in the column order they are in the input table.

If we want something different, that's probably an interface change and outside the scope of the this bugfix. Though we could certainly warn on duplicates, but it would be more code, and I like deleting code, not adding more. 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just a tiny tiny bit worried because the behavior is changing from explicit (ugly traceback) to implicit (silently dropping duplicate). I cannot decide if that is better or not. I'll defer to other votable maintainers. Thanks for the clarification!

@jdavies-st jdavies-st Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stvoutsin any thoughts on whether we should raise an exception with an informative message if someone lists the same column more than once in columns=[...]? Or silently just give them the column once?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do see the worry, but all things considered my inclination (at least for this bugfix) is to return it once, since that's what TABLEDATA already does and we're just matching it for BINARY/BINARY2.

Worth noting the IndexError before wasn't really a check against duplicates, it was the binary reader going past the end of the row because colnumbers didn't line up with the output columns. But It never mentions duplicates and only happens with certain serializations.

Since columns= doesn't control output order my reading of it is as "which columns do I want" rather than an exact spec so you still get everything you asked for.
Returning it twice would mean making up a second name since the array can't have duplicate field names, which feels wrong to me

Whether to warn is a good question, I'd lean towards not warning. TABLEDATA returns the column once and it doesn't sound like this is causing problems.
It's very possible the duplicate is a typo for another column, but they'd hit a KeyError the first time they try to use it, which I don't think is much worse than if we had warned them.

Either way if we did want to change the behaviour, warning or raising an error, I'd propose we do it as a follow-up rather than in this fix. Happy to go the other way if others feel strongly though.

@jdavies-st jdavies-st Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after some poking around, trying to understand what's going on, I see that since astropy 7.0.0, when the format is tabledata_format="tabledata" and you list the same column twice, you get it back once, and it is silent on the duplication. So this PR is just aligning binary/binary2 formats with existing tabledata format behavior.

Before astropy 7, all formats (tabledata/binary/binary2) returned a masked array table with just the single column having data, or (nans), and this goes way back to when columns=[...] was introduced. So it has been this way for years. It was only the astropy 7+ fix of returning a table without the columns instead of as masked that introduced the regression fixed here.

On main and tag 7.2.2:

from astropy.table import Table
from astropy.io.votable import parse_single_table

Table({"a": [1.0, 2.0], "b": [3.0, 4.0]}).write(
    "test.vot", format="votable", tabledata_format="tabledata"
)

parsed = parse_single_table("test.vot", columns=["a", "a"])
print(parsed)
#  a 
# ---
# 1.0
# 2.0

With astropy 6, tabledata format returned a masked array table when you asked for "a" twice:

 a   b 
--- ---
1.0  --
2.0  --

and binary/binary2 format returned nans:

 a   b 
--- ---
1.0 nan
2.0 nan

So it has never raised except for binary/binary2 format in astropy 7+, which we are now fixing in this PR.

I think if we wanted it to raise an exception intentionally, then we should change that for all 3 formats, and it would be an API change. And probably separate from this PR. Thoughts?

@pllim pllim added this to the v8.1.0 milestone Oct 1, 2026
@pllim pllim added the Bug label Oct 1, 2026

@stvoutsin stvoutsin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on this! Is it worth adding a quick line in the columns docstring that says the output is in file order, in case there is a user expectation that they would be returned in the order they provided?

Also while reading through the relevant bits to remind myself how this part works, I spotted a bug, not affected by your changes, which I'll prep a different PR for rather than tagging it on here.
(The bug is in the index path where the range check is off by one)

@jdavies-st
jdavies-st force-pushed the votable-binary-column-order branch from f4facd0 to f253c3e Compare October 2, 2026 07:36
@jdavies-st

Copy link
Copy Markdown
Contributor Author

Is it worth adding a quick line in the columns docstring that says the output is in file order, in case there is a user expectation that they would be returned in the order they provided?

Done.

The columns arg is only documented in parse(), whereas in the other user-facing functions for Table.read(format="votable") and votable.parse_single_table we get the ubiquitous "**kwargs" 😱, and in the docstring it reads "See parse for a description of the keyword arguments." Of course we have this everywhere in astropy.

And looking at this closely, I noticed a small quirk. If I pass Table.read("file.vot", format="votable", columns=["a", "b"]) and the input is a file on disk, then the file is parsed with columns=... and my resulting table has 2 columns. If I pass Table.read() an already-parsed VOTableFile, then all those kwargs are ignored, including columns=.... So if the table has more than 2 columns, you get back all of them. A small quirk, but it is because columns only gets passed to parse().

- Reading a BINARY or BINARY2 table with columns= listed in a different
  order from the file put each column's values into another column.
  When the columns had different types it raised ValueError instead.
  TABLEDATA was not affected.
- The selected columns are now always read in file order, which is the
  order the result array holds them in.
- A column named twice in columns= is now read once, as TABLEDATA
  already did, instead of raising IndexError for BINARY and BINARY2.
- Add tests reading two columns in reverse order, and one column named
  twice, in all three formats.
@jdavies-st
jdavies-st force-pushed the votable-binary-column-order branch from 65c8703 to 19ceedd Compare October 9, 2026 08:45

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VOTable columns= in a different order from the file mixes up BINARY and BINARY2 columns

3 participants