I think you need DataFrame.pivot_table witg aggfunc = ''.join or 
another that is valid for str type.
new_df = (df.pivot_table(index = 'id',columns = 'sem',
                         values = 'stu',aggfunc = ''.join)
            .rename_axis(columns = None,index = None))
print(new_df)
  sem1 sem2 sem3
1    B    A  NaN
2    A  NaN    A
You could use another function to treat the values deduplicated for the same ID and sem, for example first, although the way to not lose information here is ''.join
UPDATE
print(df)
   id   sem stu
0   1  sem2   A
1   1  sem1   B
2   1  sem1   A
3   2  sem1   A
4   2  sem3   A
new_df=( df.assign(count=df.groupby(['id','sem']).cumcount())
           .pivot_table(index = 'id',columns = ['sem','count'],
                        values = 'stu',aggfunc = ''.join)
           .rename_axis(columns = [None,None],index = None) )
print(new_df)
  sem1      sem2 sem3
     0    1    0    0
1    B    A    A  NaN
2    A  NaN  NaN    A
new_df=( df.assign(count=df.groupby(['id','sem']).cumcount())
           .pivot_table(index = ['id','count'],columns = 'sem',
                        values = 'stu',aggfunc = ''.join)
           .rename_axis(columns = None,index = [None,None]) )
print(new_df)
    sem1 sem2 sem3
1 0    B    A  NaN
  1    A  NaN  NaN
2 0    A  NaN    A
Solution without MultIndex:
new_df=( df.assign(count=df.groupby(['id','sem']).cumcount())
           .pivot_table(index = 'id',columns = ['sem','count'],
                        values = 'stu',aggfunc = ''.join)
           .rename_axis(columns = [None,None],index = None) )
#Solution with duplicates names of columns
#new_df.columns = new_df.columns.droplevel(1)
#  sem1 sem1 sem2 sem3
#1    B    C    A  NaN
#2    A  NaN  NaN    A
new_df.columns = [f'{x}_{y}' for x,y in new_df.columns]
print(new_df)
  sem1_0 sem1_1 sem2_0 sem3_0
1      B      C      A    NaN
2      A    NaN    NaN      A